// app-state.jsx — local-first app data and helpers


const CURRENCIES = [
  { code: "GBP", label: "British Pound", symbol: "£" },
  { code: "USD", label: "US Dollar", symbol: "$" },
  { code: "EUR", label: "Euro", symbol: "€" },
  { code: "CAD", label: "Canadian Dollar", symbol: "C$" },
  { code: "AUD", label: "Australian Dollar", symbol: "A$" },
];

const BUSINESS_TYPES = [
  { id: "classes", label: "Classes", desc: "Group classes with limited spaces per session, term bookings, waivers, and live capacity tracking." },
  { id: "pool_hire", label: "Private Pool Hire", desc: "Hourly private pool slots, rules, payments, and blocked maintenance times." },
  { id: "beauty", label: "Beautician", desc: "Facials, massage, brows, lashes, deposits, and treatment questions." },
  { id: "nails", label: "Nail Bar", desc: "Manicures, pedicures, nail art, staff-ready service durations and prices." },
];

function uid(prefix = "id") {
  return `${prefix}-${Math.random().toString(36).slice(2, 9)}-${Date.now().toString(36)}`;
}

function todayIso() {
  return new Date().toISOString().slice(0, 10);
}

function datePartsToIso(d) {
  const yyyy = String(d.year);
  const mm = String(d.month + 1).padStart(2, "0");
  const dd = String(d.day).padStart(2, "0");
  return `${yyyy}-${mm}-${dd}`;
}

function isoToDateParts(iso) {
  const [year, month, day] = iso.split("-").map(Number);
  return { year, month: month - 1, day };
}

function minutesToTime(total) {
  const h24 = Math.floor(total / 60);
  const mins = total % 60;
  const suffix = h24 >= 12 ? "PM" : "AM";
  const h12 = ((h24 + 11) % 12) + 1;
  return `${h12}:${String(mins).padStart(2, "0")} ${suffix}`;
}

function timeToMinutes(time) {
  if (!time) return 0;
  if (time.includes(":") && !time.includes(" ")) {
    const [h, m] = time.split(":").map(Number);
    return h * 60 + m;
  }
  const [, hh, mm, suffix] = time.match(/(\d+):(\d+)\s*(AM|PM)/i) || [];
  let h = Number(hh || 0);
  const m = Number(mm || 0);
  if (suffix?.toUpperCase() === "PM" && h !== 12) h += 12;
  if (suffix?.toUpperCase() === "AM" && h === 12) h = 0;
  return h * 60 + m;
}

function formatIsoDate(iso, opts = {}) {
  if (!iso) return "—";
  const date = new Date(`${iso}T12:00:00`);
  return date.toLocaleDateString("en-US", {
    weekday: opts.weekday || undefined,
    month: "short",
    day: "numeric",
    year: opts.year ? "numeric" : undefined,
  });
}

function normalizeHex(value, fallback) {
  const hex = String(value || "").trim();
  return /^#[0-9a-f]{6}$/i.test(hex) ? hex : fallback;
}

function hexToRgb(hex) {
  const clean = normalizeHex(hex, "#006e78").slice(1);
  return {
    r: parseInt(clean.slice(0, 2), 16),
    g: parseInt(clean.slice(2, 4), 16),
    b: parseInt(clean.slice(4, 6), 16),
  };
}

function shadeHex(hex, amount = 18) {
  const { r, g, b } = hexToRgb(hex);
  const channel = (value) => Math.max(0, Math.min(255, value + amount)).toString(16).padStart(2, "0");
  return `#${channel(r)}${channel(g)}${channel(b)}`;
}

function getLogoText(profile = {}) {
  return String(profile.logoText || profile.initials || profile.brandName?.slice(0, 1) || "N").slice(0, 3).toUpperCase();
}

function getBrandStyle(profile = {}) {
  const accent = normalizeHex(profile.accent, "#006e78");
  const logoBg = normalizeHex(profile.logoBg, accent);
  const logoColor = profile.logoColor || "#ffffff";
  const rgb = hexToRgb(accent);
  return {
    "--accent": accent,
    "--accent-hover": shadeHex(accent, 18),
    "--accent-soft": `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.16)`,
    "--brand-logo-bg": logoBg,
    "--brand-logo-fg": logoColor,
  };
}

function bookingDateTimeLabel(booking) {
  return `${formatIsoDate(booking.date, { year: true })} ${booking.time}`;
}

function makeAvailability(overrides = {}) {
  const base = [
    { name: "Monday", on: false, start: "09:00", end: "17:00" },
    { name: "Tuesday", on: false, start: "09:00", end: "17:00" },
    { name: "Wednesday", on: false, start: "09:00", end: "17:00" },
    { name: "Thursday", on: false, start: "09:00", end: "17:00" },
    { name: "Friday", on: false, start: "09:00", end: "17:00" },
    { name: "Saturday", on: false, start: "09:00", end: "17:00" },
    { name: "Sunday", on: false, start: "09:00", end: "17:00" },
  ];
  return base.map((day) => ({ ...day, ...(overrides[day.name] || {}) }));
}

function makeEvent(config) {
  return normalizeEvent({
    id: uid("evt"),
    enabled: true,
    paymentMode: Number(config.price || 0) > 0 ? "full" : "none",
    depositAmount: "",
    questions: [],
    requireTerms: false,
    termsText: "",
    useCustomAvailability: false,
    customAvailability: [],
    ...config,
  });
}

function getBusinessTemplate(type) {
  const sharedPoolQuestions = [
    { id: uid("q"), label: "How many guests/swimmers are attending?", type: "text", required: true },
    { id: uid("q"), label: "Any children, accessibility needs, or important notes?", type: "textarea", required: false },
    { id: uid("q"), label: "Emergency contact number", type: "text", required: true },
  ];
  const classQuestions = [
    { id: uid("q"), label: "What is your experience level?", type: "text", required: true },
    { id: uid("q"), label: "Any injuries, medical conditions, or anything we should know?", type: "textarea", required: false },
    { id: uid("q"), label: "Emergency contact name and number", type: "text", required: true },
  ];
  const treatmentQuestions = [
    { id: uid("q"), label: "Any allergies, sensitivities, or medical notes?", type: "textarea", required: false },
  ];
  const templates = {
    classes: {
      profile: {
        businessType: "classes",
        setupComplete: true,
        brandName: "",
        logoText: "",
        logoBg: "#006e78",
        logoColor: "#ffffff",
        accent: "#006e78",
        title: "",
        bio: "",
        bookingUrl: "",
      },
      availability: makeAvailability({
        Monday:    { on: true, start: "06:00", end: "20:00" },
        Tuesday:   { on: true, start: "06:00", end: "20:00" },
        Wednesday: { on: true, start: "06:00", end: "20:00" },
        Thursday:  { on: true, start: "06:00", end: "20:00" },
        Friday:    { on: true, start: "06:00", end: "20:00" },
        Saturday:  { on: true, start: "08:00", end: "14:00" },
      }),
      events: [],
    },
    pool_hire: {
      profile: {
        businessType: "pool_hire",
        setupComplete: true,
        brandName: "",
        logoText: "",
        logoBg: "#0070BA",
        logoColor: "#ffffff",
        accent: "#0070BA",
        title: "",
        bio: "",
        bookingUrl: "",
      },
      availability: makeAvailability({
        Monday: { on: true, start: "08:00", end: "20:00" },
        Tuesday: { on: true, start: "08:00", end: "20:00" },
        Wednesday: { on: true, start: "08:00", end: "20:00" },
        Thursday: { on: true, start: "08:00", end: "20:00" },
        Friday: { on: true, start: "08:00", end: "20:00" },
        Saturday: { on: true, start: "09:00", end: "18:00" },
        Sunday: { on: true, start: "09:00", end: "18:00" },
      }),
      events: [],
    },
    beauty: {
      profile: {
        businessType: "beauty",
        setupComplete: true,
        brandName: "",
        logoText: "",
        logoBg: "#f59e9e",
        logoColor: "#171923",
        accent: "#f59e9e",
        title: "",
        bio: "",
        bookingUrl: "",
      },
      availability: makeAvailability({ Tuesday: { on: true, start: "10:00", end: "18:00" }, Wednesday: { on: true, start: "10:00", end: "18:00" }, Thursday: { on: true, start: "10:00", end: "20:00" }, Friday: { on: true, start: "10:00", end: "18:00" }, Saturday: { on: true, start: "09:00", end: "16:00" } }),
      events: [],
    },
    nails: {
      profile: {
        businessType: "nails",
        setupComplete: true,
        brandName: "",
        logoText: "",
        logoBg: "#a78bfa",
        logoColor: "#ffffff",
        accent: "#a78bfa",
        title: "",
        bio: "",
        bookingUrl: "",
      },
      availability: makeAvailability({ Monday: { on: true, start: "09:30", end: "17:30" }, Tuesday: { on: true, start: "09:30", end: "17:30" }, Wednesday: { on: true, start: "09:30", end: "17:30" }, Thursday: { on: true, start: "09:30", end: "19:00" }, Friday: { on: true, start: "09:30", end: "17:30" }, Saturday: { on: true, start: "09:00", end: "16:00" } }),
      events: [],
    },
  };
  return templates[type] || templates.pool_hire;
}

function getInitialData() {
  // NOTE: This function is used by demo.html / demo-app.jsx for the interactive demo.
  // Real user accounts get their data from fetchUserData() (Supabase) — never from here.
  // Keep this data demo-only. Do NOT reference getInitialData() in any real auth path.
  const seed = PRESETS.generic;
  return {
    profile: {
      name: "Alex Rivera",
      initials: "AR",
      title: "Owner",
      email: "alex@example.com",
      phone: "+44 7700 900000",
      bio: "Helping clients book in for the services they love. Slots fill up — book early!",
      bookingUrl: "demo",
      brandName: "Demo Business",
      accent: "#006e78",
      logoText: "D",
      logoUrl: "",
      logoBg: "#006e78",
      logoColor: "#ffffff",
      currency: "GBP",
      theme: "dark",
      whiteLabel: false,
      businessType: "",
      setupComplete: true,
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Europe/London",
      twoFactor: { enabled: false, method: null },
    },
    events: seed.events.map((event, index) => ({
      id: uid("evt"),
      name: event.name,
      duration: event.duration,
      color: event.color,
      price: [0, 75, 120, 45][index] || 0,
      slotInterval: event.duration >= 60 ? 60 : 30,
      location: index === 0 ? "Google Meet" : "Zoom",
      description: [
        "A relaxed first appointment to get to know what you need.",
        "A focused working session with clear next steps.",
        "A longer appointment for onboarding and setup.",
        "A short check-in for existing clients.",
      ][index],
      questions: [],
      requireTerms: false,
      termsText: "",
      useCustomAvailability: false,
      customAvailability: [],
      enabled: true,
    })),
    availability: [
      { name: "Monday",    on: true,  start: "09:00", end: "17:00" },
      { name: "Tuesday",   on: true,  start: "09:00", end: "17:00" },
      { name: "Wednesday", on: true,  start: "09:00", end: "17:00" },
      { name: "Thursday",  on: true,  start: "09:00", end: "17:00" },
      { name: "Friday",    on: true,  start: "09:00", end: "15:00" },
      { name: "Saturday",  on: false, start: "10:00", end: "14:00" },
      { name: "Sunday",    on: false, start: "10:00", end: "14:00" },
    ],
    blockedDates:  [],
    blockedTimes:  [],
    bookings: seed.upcomingBookings.map((b, index) => ({
      id: uid("book"),
      date: ["2026-05-12", "2026-05-12", "2026-05-13", "2026-05-13", "2026-05-14"][index],
      time: ["10:00 AM", "1:30 PM", "9:00 AM", "3:00 PM", "11:00 AM"][index],
      eventName: b.event,
      eventId: null,
      guestName: b.guest,
      guestEmail: `${b.guest.toLowerCase().replace(/\s+/g, ".")}@example.com`,
      guestPhone: "",
      notes: "",
      status: b.status,
      source: "Demo",
    })),
    integrations: {
      "Google Calendar": { connected: false, account: "" },
      Stripe:            { connected: false, account: "" },
      PayPal:            { connected: false, account: "" },
      "Apple Calendar":  { connected: false, account: "" },
      WhatsApp:          { connected: false, account: "" },
      Square:            { connected: false, account: "" },
    },
    settings: {
      notifications: { bookEmail: true, bookSms: true, cancelEmail: true, reminders: true, reviews: false, marketing: false },
      plan: "professional",
    },
    // Platform admin data — demo only, never shown to real customers
    platform: {
      customers:     [],
      users:         [],
      billing:       [],
      announcements: [],
      settings: {
        supportEmail:     "support@nexusbooking.app",
        platformCurrency: "GBP",
        allowTrials:      true,
        trialDays:        14,
      },
    },
    activity:  [],
    locations: [],
    webhooks:  [],
  };
}

function normalizeEvent(event) {
  return {
    slotInterval: event.duration >= 60 ? 60 : 30,
    questions: [],
    requireTerms: false,
    termsText: "",
    paymentMode: Number(event.price || 0) > 0 ? "full" : "none",
    depositAmount: "",
    termWeeks: 0,
    classPrice: 0,
    useCustomAvailability: false,
    customAvailability: [],
    maxSpaces: 0,
    cancellationPolicy: "flexible",
    cancellationHours: 24,
    ...event,
  };
}

function normalizeData(data) {
  // Migrate old amber default (#efa403) to new teal brand colour (#006e78)
  const rawAccent = data.profile?.accent;
  const migratedAccent = (!rawAccent || rawAccent === "#efa403") ? "#006e78" : rawAccent;
  const migratedLogoBg = (!data.profile?.logoBg || data.profile?.logoBg === "#efa403") ? migratedAccent : data.profile.logoBg;
  const profile = {
    currency: "GBP",
    theme: "dark",
    whiteLabel: false,
    businessType: "",
    setupComplete: false,
    logoText: data.profile?.initials || "N",
    logoUrl: "",
    logoBg: migratedLogoBg,
    logoColor: "#ffffff",
    ...data.profile,
    accent: migratedAccent,
    logoBg: migratedLogoBg,
  };
  return {
    ...data,
    profile,
    events:       (data.events || []).map(normalizeEvent),
    blockedTimes: data.blockedTimes || [],
    waitlist:     data.waitlist || [],
    staff:        data.staff     || [],
    locations:    data.locations || [],
    webhooks:     data.webhooks  || [],
    platform:     data.platform || getInitialData().platform,
    // Build live integrations from profile payment fields (overrides empty seed value)
    integrations: data.integrations && Object.keys(data.integrations).length > 0
      ? data.integrations
      : {
          Stripe: { connected: !!profile.stripeConnected, publishableKey: profile.stripePublishableKey || "" },
          PayPal: { connected: !!profile.paypalConnected, clientId: profile.paypalClientId || "" },
          "Google Calendar": { connected: !!profile.googleCalendarConnected, email: profile.googleCalendarEmail || "" },
          "Apple Calendar":  { connected: !!profile.appleCalendarConnected, email: profile.appleCalendarEmail || "" },
          WhatsApp:          { connected: !!profile.whatsappConnected, phone: profile.whatsappPhone || "" },
          Twilio:            { connected: !!profile.twilioConnected, phone: profile.twilioPhone || "" },
          Square:            { connected: false },
        },
  };
}

function useAppData() {
  const [data, setData]         = React.useState(null);   // null = loading
  const [authUser, setAuthUser] = React.useState(undefined); // undefined = checking
  const userIdRef               = React.useRef(null);
  const dataRef                 = React.useRef(null);       // mirrors data for use in stable callbacks

  // ── Auth listener ─────────────────────────────────────────────
  React.useEffect(() => {
    sb.auth.getSession().then(({ data: { session } }) => {
      const u = session?.user ?? null;
      setAuthUser(u);
      userIdRef.current = u?.id ?? null;
      if (u) fetchUserData(u.id).then(setData);
      else    setData(null);
    });

    const { data: { subscription } } = sb.auth.onAuthStateChange(async (event, session) => {
      const u = session?.user ?? null;
      setAuthUser(u);
      userIdRef.current = u?.id ?? null;
      if (u && (event === "SIGNED_IN" || event === "TOKEN_REFRESHED")) {
        fetchUserData(u.id).then(setData);
      }
      if (event === "SIGNED_OUT") setData(null);
    });

    return () => subscription.unsubscribe();
  }, []);

  // ── Real-time: new bookings arrive instantly ──────────────────
  React.useEffect(() => {
    const uid_val = authUser?.id;
    if (!uid_val) return;
    const channel = sb
      .channel("bookings-rt-" + uid_val)
      .on("postgres_changes", { event: "INSERT", schema: "public", table: "bookings", filter: `user_id=eq.${uid_val}` },
        (payload) => {
          setData(d => d ? ({
            ...d,
            bookings:  [dbToBooking(payload.new), ...d.bookings],
            activity:  [{ id: uid("act"), text: `${payload.new.guest_name} booked ${payload.new.event_name}.`, at: "Just now" }, ...d.activity].slice(0, 8),
          }) : d);
        })
      .subscribe();
    return () => sb.removeChannel(channel);
  }, [authUser?.id]);

  // Keep dataRef current so stable callbacks can read latest state
  React.useEffect(() => { dataRef.current = data; }, [data]);

  // Handle Google OAuth redirect params
  React.useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const connected = params.get("google_connected");
    const gcalError = params.get("google_error");
    if (connected || gcalError) {
      const url = new URL(window.location.href);
      url.searchParams.delete("google_connected");
      url.searchParams.delete("google_error");
      window.history.replaceState({}, "", url.toString());
    }
    if (gcalError) {
      console.warn("Google Calendar OAuth error:", gcalError);
    }
  }, []);

  const ownerId = () => userIdRef.current;

  // ── Helpers ───────────────────────────────────────────────────
  const addActivity = React.useCallback((text) => {
    const id = uid("act");
    setData(d => d ? ({ ...d, activity: [{ id, text, at: "Just now" }, ...d.activity].slice(0, 8) }) : d);
    const o = ownerId();
    if (o) sb.from("activity").insert({ id, user_id: o, text });
  }, []);

  // ── Actions ───────────────────────────────────────────────────

  const addBooking = React.useCallback((booking) => {
    const events  = data?.events || [];
    const event   = events.find(e => e.id === booking.eventId) || events[0];
    if (!event) return null;
    const bookId     = uid("book");
    const manageToken = (typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : uid("tok");
    const next = {
      id: bookId, date: booking.date, time: booking.time,
      eventId: event.id, eventName: event.name,
      guestName: booking.name, guestEmail: booking.email,
      guestPhone: booking.phone || "", notes: booking.notes || "",
      answers: booking.answers || {}, location: event.location || "",
      paymentStatus: booking.paymentStatus || (Number(event.price || 0) > 0 ? "Paid" : "Not required"),
      paymentProvider: booking.paymentProvider || "",
      paymentAmount: booking.paymentAmount ?? Number(event.price || 0),
      status: "Confirmed", source: "Public page",
      manageToken,
      staffId:   booking.staffId   || null,
      staffName: booking.staffName || null,
    };
    setData(d => d ? ({
      ...d,
      bookings: [next, ...d.bookings],
      activity: [{ id: uid("act"), text: `${booking.name} booked ${event.name}.`, at: "Just now" }, ...d.activity].slice(0, 8),
    }) : d);
    const o = ownerId() || data?.userId;
    if (o) {
      sb.from("bookings").insert({
        id: bookId, user_id: o,
        event_id: event.id, event_name: event.name,
        date: next.date, time: next.time,
        guest_name: next.guestName, guest_email: next.guestEmail, guest_phone: next.guestPhone,
        notes: next.notes, answers: next.answers, location: next.location,
        status: next.status, payment_status: next.paymentStatus,
        payment_provider: next.paymentProvider, payment_amount: next.paymentAmount, source: next.source,
        manage_token: manageToken,
        staff_id:   next.staffId   || null,
        staff_name: next.staffName || null,
      }).then(() => {
        const d = dataRef.current;
        // Fire booking.created webhooks
        fireWebhooks(d?.webhooks || [], "booking.created", {
          id: bookId, date: next.date, time: next.time,
          eventName: next.eventName, guestName: next.guestName,
          guestEmail: next.guestEmail, guestPhone: next.guestPhone,
          staffName: next.staffName, paymentStatus: next.paymentStatus,
          paymentAmount: next.paymentAmount,
        });
        // Fire payment.received webhook when booking has payment
        if (next.paymentStatus === "Paid" && Number(next.paymentAmount || 0) > 0) {
          fireWebhooks(d?.webhooks || [], "payment.received", {
            bookingId: bookId, guestName: next.guestName,
            guestEmail: next.guestEmail, eventName: next.eventName,
            amount: next.paymentAmount, provider: next.paymentProvider,
          });
        }
        const VIDEO_TYPES = ["google_meet", "zoom", "teams"];
        const meetingType = VIDEO_TYPES.includes(event.location) ? event.location : null;

        // Create video meeting if event type requires one
        if (meetingType) {
          const intg = d?.integrations || {};
          const canMeet =
            (meetingType === "google_meet" && intg["Google Calendar"]?.connected) ||
            (meetingType === "zoom"        && intg["Zoom"]?.connected) ||
            (meetingType === "teams"       && intg["Microsoft Teams"]?.connected);
          if (canMeet) {
            createVideoMeeting(meetingType, o, {
              id:        next.id,
              date:      next.date,
              time:      next.time,
              eventName: next.eventName,
              guestName: next.guestName,
              guestEmail: next.guestEmail,
              guestPhone: next.guestPhone,
              notes:     next.notes,
              staffName: next.staffName,
              duration:  event.duration || 30,
            }, event.duration || 30);
          }
        }

        // Sync to Google Calendar (for non-Meet events)
        if (!meetingType && d?.integrations?.["Google Calendar"]?.connected) {
          syncCalendarEvent("create", o, {
            id:          next.id,
            date:        next.date,
            time:        next.time,
            eventName:   next.eventName,
            guestName:   next.guestName,
            guestEmail:  next.guestEmail,
            guestPhone:  next.guestPhone,
            notes:       next.notes,
            staffName:   next.staffName,
            location:    next.location,
            manageToken: next.manageToken,
          }, event.duration || 30);
        }
      });
    }
    return next;
  }, [data]);

  const updateBookingStatus = React.useCallback((id, status) => {
    setData(d => d ? ({ ...d, bookings: d.bookings.map(b => b.id === id ? { ...b, status } : b) }) : d);
    const o = ownerId();
    if (o) {
      sb.from("bookings").update({ status }).eq("id", id);
      // Delete calendar event when booking is cancelled
      if (status === "Cancelled") {
        const d = dataRef.current;
        const booking = (d?.bookings || []).find(b => b.id === id);
        if (booking?.googleCalendarEventId && d?.integrations?.["Google Calendar"]?.connected) {
          syncCalendarEvent("delete", o, {
            id,
            googleCalendarEventId: booking.googleCalendarEventId,
          }, 0);
        }
        // Fire booking.cancelled webhook
        if (booking) {
          fireWebhooks(d?.webhooks || [], "booking.cancelled", {
            id, date: booking.date, time: booking.time,
            eventName: booking.eventName, guestName: booking.guestName,
            guestEmail: booking.guestEmail,
          });
        }
      }
    }
  }, []);

  const rescheduleBooking = React.useCallback((id, patch) => {
    // patch: { date, time, status, notes }
    setData(d => d ? ({ ...d, bookings: d.bookings.map(b => b.id === id ? { ...b, ...patch } : b) }) : d);
    const o = ownerId();
    if (o) {
      sb.from("bookings").update({
        date: patch.date, time: patch.time,
        status: patch.status, notes: patch.notes,
      }).eq("id", id).eq("user_id", o);
      // Fire booking.rescheduled webhook
      {
        const d = dataRef.current;
        const booking = (d?.bookings || []).find(b => b.id === id);
        if (booking) {
          fireWebhooks(d?.webhooks || [], "booking.rescheduled", {
            id, newDate: patch.date, newTime: patch.time,
            eventName: booking.eventName, guestName: booking.guestName,
            guestEmail: booking.guestEmail,
          });
        }
      }
      // Update calendar event with new date/time
      const d = dataRef.current;
      if (d?.integrations?.["Google Calendar"]?.connected) {
        const booking = (d?.bookings || []).find(b => b.id === id);
        if (booking?.googleCalendarEventId) {
          const event = (d?.events || []).find(e => e.id === booking.eventId);
          syncCalendarEvent("update", o, {
            id,
            googleCalendarEventId: booking.googleCalendarEventId,
            date:      patch.date,
            time:      patch.time,
            eventName: booking.eventName,
            guestName: booking.guestName,
          }, event?.duration || 30);
        }
      }
    }
  }, []);

  const saveEvent = React.useCallback((event) => {
    const clean = normalizeEvent({
      ...event,
      id: event.id || uid("evt"),
      duration: Number(event.duration),
      slotInterval: Number(event.slotInterval || event.duration || 30),
      price: Number(event.price || 0),
      maxSpaces: Number(event.maxSpaces || 0),
      termWeeks: Number(event.termWeeks || 0),
      classPrice: Number(event.classPrice || 0),
      depositAmount: event.depositAmount === "" ? "" : Number(event.depositAmount || 0),
      useCustomAvailability: !!event.useCustomAvailability,
      customAvailability: (event.customAvailability || []).map(day => ({ ...day, on: !!day.on })),
      cancellationPolicy: event.cancellationPolicy || "flexible",
      cancellationHours:  Number(event.cancellationHours ?? 24),
      questions: (event.questions || []).filter(q => q.label?.trim()).map(q => ({
        ...q, id: q.id || uid("q"), label: q.label.trim(), required: !!q.required,
      })),
    });
    setData(d => {
      if (!d) return d;
      const exists = d.events.some(e => e.id === clean.id);
      return { ...d, events: exists ? d.events.map(e => e.id === clean.id ? clean : e) : [...d.events, clean] };
    });
    const o = ownerId();
    if (o) sb.from("events").upsert(eventToDb(clean, o));
    addActivity(event.id ? `${clean.name} was updated.` : `${clean.name} was added.`);
  }, [addActivity]);

  const toggleEvent = React.useCallback((id) => {
    let next;
    setData(d => {
      if (!d) return d;
      const events = d.events.map(e => { if (e.id === id) { next = !e.enabled; return { ...e, enabled: next }; } return e; });
      return { ...d, events };
    });
    const o = ownerId();
    if (o) setTimeout(() => { if (next !== undefined) sb.from("events").update({ enabled: next }).eq("id", id); }, 0);
  }, []);

  const deleteEvent = React.useCallback((id) => {
    setData(d => d ? ({ ...d, events: d.events.filter(e => e.id !== id) }) : d);
    const o = ownerId();
    if (o) sb.from("events").delete().eq("id", id);
  }, []);

  const addPoolTemplate = React.useCallback(() => {
    const poolEvent = makeEvent({
      name: "Private Pool Hire", duration: 60, slotInterval: 60, color: "#7aa7ff", price: 40,
      location: "Your Pool Address", paymentMode: "full",
      description: "One-hour private hire of the pool for your group.",
      questions: [
        { id: uid("q"), label: "How many swimmers are attending?", type: "text", required: true },
        { id: uid("q"), label: "Emergency contact number", type: "text", required: true },
      ],
      requireTerms: true, termsText: "I agree to the pool hire safety rules and cancellation terms.",
    });
    setData(d => d ? ({
      ...d,
      events: [...d.events, poolEvent],
      activity: [{ id: uid("act"), text: "Private Pool Hire template was added.", at: "Just now" }, ...d.activity].slice(0, 8),
    }) : d);
    const o = ownerId();
    if (o) sb.from("events").insert(eventToDb(poolEvent, o));
  }, []);

  const saveAvailability = React.useCallback((availability) => {
    setData(d => d ? ({ ...d, availability }) : d);
    const o = ownerId();
    if (o) sb.from("availability").upsert(
      availability.map(day => ({ user_id: o, day_name: day.name, enabled: day.on, start_time: day.start, end_time: day.end }))
    );
    addActivity("Availability was updated.");
  }, [addActivity]);

  const addBlockedTime = React.useCallback((exception) => {
    const id = uid("blocktime");
    const item = { id, ...exception };
    setData(d => d ? ({ ...d, blockedTimes: [...(d.blockedTimes || []), item] }) : d);
    const o = ownerId();
    if (o) sb.from("blocked_times").insert({ id, user_id: o, date: exception.date, start_time: exception.start, end_time: exception.end, reason: exception.reason });
  }, []);

  const removeBlockedTime = React.useCallback((id) => {
    setData(d => d ? ({ ...d, blockedTimes: (d.blockedTimes || []).filter(b => b.id !== id) }) : d);
    const o = ownerId();
    if (o) sb.from("blocked_times").delete().eq("id", id);
  }, []);

  const addBlockedDate = React.useCallback((date, reason) => {
    addBlockedTime({ date, start: "00:00", end: "23:59", reason });
  }, [addBlockedTime]);

  const removeBlockedDate = React.useCallback((id) => {
    removeBlockedTime(id);
  }, [removeBlockedTime]);

  const connectIntegration = React.useCallback((name) => {
    setData(d => d ? ({ ...d, integrations: { ...d.integrations, [name]: { connected: true } } }) : d);
    addActivity(`${name} was connected.`);
  }, [addActivity]);

  const disconnectIntegration = React.useCallback((name) => {
    setData(d => d ? ({ ...d, integrations: { ...d.integrations, [name]: { connected: false } } }) : d);
  }, []);

  // Real payment credential save — updates profile fields + payment_credentials table
  const savePaymentSetup = React.useCallback(async (provider, publicKey, secretKey) => {
    const o = ownerId();
    if (!o) return;
    if (provider === "Stripe") {
      await saveStripeCredentials(o, publicKey, secretKey);
      setData(d => d ? ({
        ...d,
        profile: { ...d.profile, stripePublishableKey: publicKey, stripeConnected: !!(publicKey && secretKey) },
        integrations: { ...d.integrations, Stripe: { connected: !!(publicKey && secretKey), publishableKey: publicKey } },
      }) : d);
      addActivity("Stripe was connected.");
    }
    if (provider === "PayPal") {
      await savePaypalCredentials(o, publicKey, secretKey);
      setData(d => d ? ({
        ...d,
        profile: { ...d.profile, paypalClientId: publicKey, paypalConnected: !!(publicKey && secretKey) },
        integrations: { ...d.integrations, PayPal: { connected: !!(publicKey && secretKey), clientId: publicKey } },
      }) : d);
      addActivity("PayPal was connected.");
    }
  }, [addActivity]);

  const removePaymentSetup = React.useCallback(async (provider) => {
    const o = ownerId();
    if (!o) return;
    await removePaymentProvider(o, provider);
    if (provider === "Stripe") {
      setData(d => d ? ({
        ...d,
        profile: { ...d.profile, stripePublishableKey: "", stripeConnected: false },
        integrations: { ...d.integrations, Stripe: { connected: false, publishableKey: "" } },
      }) : d);
    }
    if (provider === "PayPal") {
      setData(d => d ? ({
        ...d,
        profile: { ...d.profile, paypalClientId: "", paypalConnected: false },
        integrations: { ...d.integrations, PayPal: { connected: false, clientId: "" } },
      }) : d);
    }
  }, []);

  const connectGoogleCalendar = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    // Strip any existing query params so callback redirects cleanly
    const returnUrl = window.location.href.split("?")[0];
    const result = await getGoogleAuthUrl(o, returnUrl);
    if (result.authUrl) {
      window.location.href = result.authUrl;
    } else {
      console.error("Google auth URL error:", result.error);
    }
  }, []);

  const disconnectGoogleCalendar = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    await clearGoogleCalendar(o);
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, googleCalendarConnected: false, googleCalendarEmail: "" },
      integrations: { ...d.integrations, "Google Calendar": { connected: false, email: "" } },
    }) : d);
    addActivity("Google Calendar was disconnected.");
  }, [addActivity]);

  const connectMicrosoftTeams = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    const returnUrl = window.location.href.split("?")[0];
    const result = await getMicrosoftAuthUrl(o, returnUrl);
    if (result.authUrl) {
      window.location.href = result.authUrl;
    } else {
      console.error("Microsoft auth URL error:", result.error);
    }
  }, []);

  const disconnectMicrosoftTeams = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    await clearMicrosoftTeams(o);
    setData(d => d ? ({
      ...d,
      integrations: { ...d.integrations, "Microsoft Teams": { connected: false } },
    }) : d);
    addActivity("Microsoft Teams was disconnected.");
  }, [addActivity]);

  const saveZoomSetup = React.useCallback(async (accountId, clientId, clientSecret) => {
    const o = ownerId();
    if (!o) return;
    await saveZoomCredentials(o, accountId, clientId, clientSecret);
    setData(d => d ? ({
      ...d,
      integrations: { ...d.integrations, Zoom: { connected: true } },
    }) : d);
    addActivity("Zoom was connected.");
  }, [addActivity]);

  const removeZoomSetup = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    await removeZoomCredentials(o);
    setData(d => d ? ({
      ...d,
      integrations: { ...d.integrations, Zoom: { connected: false } },
    }) : d);
    addActivity("Zoom was disconnected.");
  }, [addActivity]);

  const saveTwilioSetup = React.useCallback(async (accountSid, authToken, fromNumber) => {
    const o = ownerId();
    if (!o) return;
    await saveTwilioCredentials(o, accountSid, authToken, fromNumber);
    const connected = !!(accountSid && authToken && fromNumber);
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, twilioConnected: connected, twilioPhone: fromNumber || "" },
      integrations: { ...d.integrations, Twilio: { connected, phone: fromNumber || "" } },
    }) : d);
    addActivity(connected ? "Twilio SMS connected." : "Twilio SMS disconnected.");
  }, [addActivity]);

  const removeTwilioSetup = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    await removeTwilioCredentials(o);
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, twilioConnected: false, twilioPhone: "" },
      integrations: { ...d.integrations, Twilio: { connected: false, phone: "" } },
    }) : d);
    addActivity("Twilio SMS disconnected.");
  }, [addActivity]);

  const saveWhatsAppSetup = React.useCallback(async (phone) => {
    const o = ownerId();
    if (!o) return;
    await window.saveWhatsAppSetup(o, phone);
    const connected = !!phone;
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, whatsappConnected: connected, whatsappPhone: phone || "" },
      integrations: { ...d.integrations, WhatsApp: { connected, phone: phone || "" } },
    }) : d);
    addActivity(connected ? "WhatsApp Business connected." : "WhatsApp Business disconnected.");
  }, [addActivity]);

  const removeWhatsAppSetup = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    await window.removeWhatsAppSetup(o);
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, whatsappConnected: false, whatsappPhone: "" },
      integrations: { ...d.integrations, WhatsApp: { connected: false, phone: "" } },
    }) : d);
    addActivity("WhatsApp Business disconnected.");
  }, [addActivity]);

  const saveAppleCalendarSetup = React.useCallback(async (appleId, appPassword) => {
    const o = ownerId();
    if (!o) return;
    await window.saveAppleCalendarSetup(o, appleId, appPassword);
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, appleCalendarConnected: true, appleCalendarEmail: appleId },
      integrations: { ...d.integrations, "Apple Calendar": { connected: true, email: appleId } },
    }) : d);
    addActivity("Apple Calendar connected.");
  }, [addActivity]);

  const removeAppleCalendarSetup = React.useCallback(async () => {
    const o = ownerId();
    if (!o) return;
    await window.removeAppleCalendarSetup(o);
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, appleCalendarConnected: false, appleCalendarEmail: "" },
      integrations: { ...d.integrations, "Apple Calendar": { connected: false, email: "" } },
    }) : d);
    addActivity("Apple Calendar disconnected.");
  }, [addActivity]);

  const updateProfile = React.useCallback((patch) => {
    let updated;
    setData(d => { if (!d) return d; updated = { ...d.profile, ...patch }; return { ...d, profile: updated }; });
    const o = ownerId();
    if (o) setTimeout(() => {
      if (!updated) return;
      sb.from("profiles").upsert(profileToDb(updated, o)).then(({ error }) => {
        if (error) {
          console.error("Profile save failed:", error);
          // Show a simple banner so the user knows something went wrong
          const el = document.createElement("div");
          el.textContent = "⚠ Profile save failed — " + (error.message || "unknown error");
          Object.assign(el.style, {
            position:"fixed", bottom:20, left:"50%", transform:"translateX(-50%)",
            background:"#c0392b", color:"#fff", padding:"10px 18px",
            borderRadius:8, fontSize:13, zIndex:9999, boxShadow:"0 4px 12px rgba(0,0,0,.3)"
          });
          document.body.appendChild(el);
          setTimeout(() => el.remove(), 6000);
        }
      });
    }, 0);
    addActivity("Profile was updated.");
  }, [addActivity]);

  const updateNotifications = React.useCallback((notifications) => {
    setData(d => d ? ({ ...d, notifications }) : d);
    const o = ownerId();
    if (o) sb.from("profiles").update({ notifications }).eq("id", o);
  }, []);

  const applyBusinessType = React.useCallback((type) => {
    const template = getBusinessTemplate(type);
    const o = ownerId();
    setData(d => d ? ({
      ...d,
      profile: { ...d.profile, ...template.profile },
      events: template.events || [],
      availability: template.availability || d.availability,
      blockedTimes: template.blockedTimes || [],
      blockedDates: [],
      bookings: [],
      activity: [{ id: uid("act"), text: `${BUSINESS_TYPES.find(b => b.id === type)?.label || "Business"} setup applied.`, at: "Just now" }],
    }) : d);
    if (o) {
      sb.from("profiles").upsert(profileToDb(template.profile, o));
      sb.from("events").delete().eq("user_id", o).then(() => {
        if (template.events?.length) sb.from("events").insert(template.events.map(e => eventToDb(e, o)));
      });
      if (template.availability?.length) sb.from("availability").upsert(
        template.availability.map(day => ({ user_id: o, day_name: day.name, enabled: day.on, start_time: day.start, end_time: day.end }))
      );
    }
  }, []);

  const resetDemoData = React.useCallback(() => {
    if (data?.profile?.businessType) applyBusinessType(data.profile.businessType);
  }, [data, applyBusinessType]);

  const addAnnouncement = React.useCallback((announcement) => {
    setData(d => d ? ({
      ...d,
      platform: { ...d.platform, announcements: [{ id: uid("ann"), date: todayIso(), status: "Draft", ...announcement }, ...d.platform.announcements] },
    }) : d);
  }, []);

  const updateCustomerStatus = React.useCallback((id, status) => {
    setData(d => d ? ({
      ...d,
      platform: { ...d.platform, customers: d.platform.customers.map(c => c.id === id ? { ...c, status } : c) },
    }) : d);
  }, []);

  const updatePlatformSettings = React.useCallback((settings) => {
    setData(d => d ? ({ ...d, platform: { ...d.platform, settings: { ...d.platform.settings, ...settings } } }) : d);
  }, []);

  const saveStaff = React.useCallback((member) => {
    const clean = {
      ...member,
      id: member.id || uid("stf"),
      color: member.color || "#7aa7ff",
      active: member.active !== false,
      serviceIds: member.serviceIds || [],
      customAvailability: (member.customAvailability || []).map(d => ({ ...d, on: !!d.on })),
    };
    setData(d => {
      if (!d) return d;
      const exists = (d.staff || []).some(s => s.id === clean.id);
      return { ...d, staff: exists ? d.staff.map(s => s.id === clean.id ? clean : s) : [...(d.staff || []), clean] };
    });
    const o = ownerId();
    if (o) saveStaffMember(clean, o);
    addActivity(member.id ? `${clean.name} was updated.` : `${clean.name} was added to the team.`);
  }, [addActivity]);

  const deleteStaff = React.useCallback((id) => {
    setData(d => d ? ({ ...d, staff: (d.staff || []).filter(s => s.id !== id) }) : d);
    const o = ownerId();
    if (o) deleteStaffMember(id);
  }, []);

  const toggleStaff = React.useCallback((id) => {
    let next;
    setData(d => {
      if (!d) return d;
      const staff = (d.staff || []).map(s => { if (s.id === id) { next = !s.active; return { ...s, active: next }; } return s; });
      return { ...d, staff };
    });
    const o = ownerId();
    if (o) setTimeout(() => { if (next !== undefined) sb.from("staff").update({ active: next }).eq("id", id); }, 0);
  }, []);

  // ── Locations ─────────────────────────────────────────────────
  const saveLocation = React.useCallback((loc) => {
    const clean = { ...loc, id: loc.id || uid("loc") };
    setData(d => {
      if (!d) return d;
      const exists = (d.locations || []).some(l => l.id === clean.id);
      return { ...d, locations: exists ? d.locations.map(l => l.id === clean.id ? clean : l) : [...(d.locations || []), clean] };
    });
    addActivity(loc.id ? `Location "${clean.name}" was updated.` : `"${clean.name}" was added as a location.`);
  }, [addActivity]);

  const deleteLocation = React.useCallback((id) => {
    setData(d => d ? ({
      ...d,
      locations: (d.locations || []).filter(l => l.id !== id),
      // Remove this location from any staff who had it assigned
      staff: (d.staff || []).map(s => ({
        ...s,
        locationIds: (s.locationIds || []).filter(lid => lid !== id),
      })),
    }) : d);
    addActivity("A location was removed.");
  }, [addActivity]);

  // ── Webhooks ───────────────────────────────────────────────────
  const saveWebhook = React.useCallback((hook) => {
    const clean = { ...hook, id: hook.id || uid("wh") };
    setData(d => {
      if (!d) return d;
      const exists = (d.webhooks || []).some(w => w.id === clean.id);
      return { ...d, webhooks: exists ? d.webhooks.map(w => w.id === clean.id ? clean : w) : [...(d.webhooks || []), clean] };
    });
    addActivity(hook.id ? `Webhook "${clean.name || clean.url}" was updated.` : `Webhook "${clean.name || clean.url}" was added.`);
  }, [addActivity]);

  const deleteWebhook = React.useCallback((id) => {
    setData(d => d ? ({ ...d, webhooks: (d.webhooks || []).filter(w => w.id !== id) }) : d);
    addActivity("A webhook was removed.");
  }, [addActivity]);

  const notifyWaitlistEntry = React.useCallback((id) => {
    setData(d => d ? ({
      ...d,
      waitlist: (d.waitlist || []).map(w => w.id === id ? { ...w, status: "Notified" } : w),
    }) : d);
    notifyWaitlistGuest(id);
    addActivity("Waitlist guest was notified.");
  }, [addActivity]);

  const removeWaitlistEntry = React.useCallback((id) => {
    setData(d => d ? ({
      ...d,
      waitlist: (d.waitlist || []).filter(w => w.id !== id),
    }) : d);
    removeFromWaitlist(id);
  }, []);

  return {
    data,
    authUser,
    actions: {
      addActivity, addBooking, updateBookingStatus, rescheduleBooking,
      saveEvent, toggleEvent, deleteEvent, addPoolTemplate,
      saveAvailability, addBlockedDate, removeBlockedDate, addBlockedTime, removeBlockedTime,
      connectIntegration, disconnectIntegration,
      connectGoogleCalendar, disconnectGoogleCalendar,
      connectMicrosoftTeams, disconnectMicrosoftTeams,
      saveZoomSetup, removeZoomSetup,
      saveTwilioSetup, removeTwilioSetup,
      saveWhatsAppSetup, removeWhatsAppSetup,
      saveAppleCalendarSetup, removeAppleCalendarSetup,
      savePaymentSetup, removePaymentSetup,
      updateProfile, updateNotifications, resetDemoData, applyBusinessType,
      addAnnouncement, updateCustomerStatus, updatePlatformSettings,
      notifyWaitlistEntry, removeWaitlistEntry,
      saveStaff, deleteStaff, toggleStaff,
      saveLocation, deleteLocation,
      saveWebhook, deleteWebhook,
    },
  };
}

// staff param (optional): specific staff object to check their schedule + bookings
// If staff.id === "any" treat as no staff filter (caller handles union)
// ── Webhook delivery ──────────────────────────────────────────────────────
function fireWebhooks(webhooks, eventType, payload) {
  if (!webhooks || webhooks.length === 0) return;
  const active = webhooks.filter(h => h.active !== false && (h.events || []).includes(eventType));
  active.forEach(hook => {
    const body = JSON.stringify({ event: eventType, timestamp: new Date().toISOString(), data: payload });
    const headers = { "Content-Type": "application/json" };
    if (hook.secret) headers["X-Nexus-Signature"] = hook.secret;
    fetch(hook.url, { method: "POST", headers, body }).catch(() => {});
  });
}

function getAvailableTimes({ data, event, dateIso, staff }) {
  if (!event || !dateIso) return [];
  const date = new Date(`${dateIso}T12:00:00`);
  const today = new Date(`${todayIso()}T00:00:00`);
  if (date < today) return [];

  const blocked = (data.blockedDates || []).some((b) => b.date === dateIso);
  if (blocked) return [];

  // Schedule priority: staff custom → event custom → global
  const staffSchedule = staff && staff.id !== "any" && staff.useCustomAvailability && staff.customAvailability?.length
    ? staff.customAvailability : null;
  const schedule = staffSchedule
    || (event.useCustomAvailability && event.customAvailability?.length ? event.customAvailability : data.availability);

  const day = schedule[date.getDay() === 0 ? 6 : date.getDay() - 1];
  if (!day?.on) return [];

  const start        = timeToMinutes(day.start);
  const end          = timeToMinutes(day.end);
  const duration     = Number(event.duration || 30);
  const slotInterval = Number(event.slotInterval || 30);
  const bufferAfter  = Number(event.bufferAfter || 0);

  // Bookings relevant to this staff member (or all if no staff filter)
  const relevantBookings = data.bookings.filter((b) => {
    if (b.date !== dateIso || b.status === "Cancelled") return false;
    if (staff && staff.id !== "any") return b.staffId === staff.id;
    return true;
  });

  // Build occupied ranges: each booking blocks [start, start + duration + bufferAfter)
  // so the buffer gap after each appointment is respected
  const occupiedRanges = relevantBookings.map((b) => {
    const bEvent  = (data.events || []).find((e) => e.id === b.eventId);
    const bDur    = Number(bEvent?.duration   || 30);
    const bBuf    = Number(bEvent?.bufferAfter || 0);
    const bStart  = timeToMinutes(b.time);
    return { start: bStart, end: bStart + bDur + bBuf, time: b.time, eventId: b.eventId };
  });

  const blockedRanges = (data.blockedTimes || [])
    .filter((b) => b.date === dateIso)
    .map((b) => ({ start: timeToMinutes(b.start), end: timeToMinutes(b.end) }));

  const isClassMode = Number(event.maxSpaces || 0) > 0;
  const times = [];
  for (let t = start; t + duration <= end; t += slotInterval) {
    const label   = minutesToTime(t);
    const slotEnd = t + duration;
    const overlapsBlockedRange = blockedRanges.some((range) => t < range.end && slotEnd > range.start);
    if (overlapsBlockedRange) continue;
    if (isClassMode) {
      // Class mode: count bookings at this exact slot
      const booked = data.bookings.filter((b) => {
        if (b.date !== dateIso || b.eventId !== event.id || b.time !== label || b.status === "Cancelled") return false;
        if (staff && staff.id !== "any") return b.staffId === staff.id;
        return true;
      }).length;
      if (booked < Number(event.maxSpaces)) times.push(label);
    } else {
      // Range-based conflict: slot [t, t+duration) must not overlap any occupied range
      // (occupied ranges already include each booking's bufferAfter)
      const conflicts = occupiedRanges.some((r) => t < r.end && slotEnd > r.start);
      if (!conflicts) times.push(label);
    }
  }
  return times;
}

// Returns time slots where maxSpaces is reached (class mode only)
// staff param optional — same logic as getAvailableTimes
function getFullTimes({ data, event, dateIso, staff }) {
  if (!event || !dateIso || !Number(event.maxSpaces || 0)) return [];
  const date = new Date(`${dateIso}T12:00:00`);
  const today = new Date(`${todayIso()}T00:00:00`);
  if (date < today) return [];

  const blocked = (data.blockedDates || []).some(b => b.date === dateIso);
  if (blocked) return [];

  const staffSchedule = staff && staff.id !== "any" && staff.useCustomAvailability && staff.customAvailability?.length
    ? staff.customAvailability : null;
  const schedule = staffSchedule
    || (event.useCustomAvailability && event.customAvailability?.length ? event.customAvailability : data.availability);

  const day = schedule[date.getDay() === 0 ? 6 : date.getDay() - 1];
  if (!day?.on) return [];

  const start        = timeToMinutes(day.start);
  const end          = timeToMinutes(day.end);
  const duration     = Number(event.duration || 30);
  const slotInterval = Number(event.slotInterval || 30);
  const blockedRanges = (data.blockedTimes || [])
    .filter(b => b.date === dateIso)
    .map(b => ({ start: timeToMinutes(b.start), end: timeToMinutes(b.end) }));

  const fullTimes = [];
  for (let t = start; t + duration <= end; t += slotInterval) {
    const label   = minutesToTime(t);
    const slotEnd = t + duration;
    const overlaps = blockedRanges.some(r => t < r.end && slotEnd > r.start);
    if (overlaps) continue;
    const booked = data.bookings.filter(b => {
      if (b.date !== dateIso || b.eventId !== event.id || b.time !== label || b.status === "Cancelled") return false;
      if (staff && staff.id !== "any") return b.staffId === staff.id;
      return true;
    }).length;
    if (booked >= Number(event.maxSpaces)) fullTimes.push(label);
  }
  return fullTimes;
}

function getSlotInfo({ data, event, dateIso, time }) {
  if (!event || !Number(event.maxSpaces || 0)) return null;
  const booked = data.bookings.filter(
    (b) => b.date === dateIso && b.eventId === event.id && b.time === time && b.status !== "Cancelled"
  ).length;
  const total = Number(event.maxSpaces);
  return { booked, total, available: total - booked };
}

function downloadCsv(filename, rows) {
  const csv = rows.map((row) => row.map((cell) => `"${String(cell ?? "").replaceAll('"', '""')}"`).join(",")).join("\n");
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.click();
  URL.revokeObjectURL(url);
}

function getCurrency(code) {
  return CURRENCIES.find((c) => c.code === code) || CURRENCIES[0];
}

function formatMoney(amount, currencyCode) {
  const currency = getCurrency(currencyCode);
  return `${currency.symbol}${Number(amount || 0).toLocaleString()}`;
}

window.useAppData = useAppData;
window.datePartsToIso = datePartsToIso;
window.isoToDateParts = isoToDateParts;
window.formatIsoDate = formatIsoDate;
window.bookingDateTimeLabel = bookingDateTimeLabel;
window.getAvailableTimes = getAvailableTimes;
window.getFullTimes      = getFullTimes;
window.getSlotInfo       = getSlotInfo;
// ── Subdomain URL helpers ──────────────────────────────────────
const APP_ORIGIN  = "https://app.nexusbookings.app";
const BOOK_ORIGIN = "https://book.nexusbookings.app";

// Returns the public booking URL for a given slug
function getBookingUrl(slug) {
  return slug ? `${BOOK_ORIGIN}/${slug}` : BOOK_ORIGIN;
}

window.APP_ORIGIN   = APP_ORIGIN;
window.BOOK_ORIGIN  = BOOK_ORIGIN;
window.getBookingUrl = getBookingUrl;
window.downloadCsv = downloadCsv;
window.uid = uid;
window.todayIso = todayIso;
window.CURRENCIES = CURRENCIES;
window.getCurrency = getCurrency;
window.formatMoney = formatMoney;
window.normalizeEvent = normalizeEvent;
window.makeAvailability = makeAvailability;
window.BUSINESS_TYPES = BUSINESS_TYPES;
window.getBusinessTemplate = getBusinessTemplate;
window.getBrandStyle = getBrandStyle;
window.getLogoText = getLogoText;
