/* ClientBase, Couche de données Supabase (étape 3/6)

   Expose window.cbCloud avec :
   - loadAll()                          → {clients, appointments, invoices, stock, notifications,
                                            business, bookingSettings, vacations, fideliteRules}
   - upsertClient / removeClient
   - upsertAppointment / removeAppointment
   - upsertInvoice / removeInvoice / togglePaid
   - upsertStockItem / setStockQty / removeStockItem
   - upsertBusiness (update uniquement, la ligne est créée par le trigger)
   - upsertBookingSettings
   - addVacation / removeVacation
   - setFideliteRules
   - markAllNotificationsRead / removeNotification

   Les fonctions de lecture renvoient l'état converti en camelCase (forme
   attendue par l'UI historique localStorage). Les fonctions d'écriture
   renvoient une promise, l'UI met à jour optimistiquement et montre
   un toast warn si l'appel échoue.
*/

const _sb = () => window.cbSupabase;
const _uid = () => window.cbAuth && window.cbAuth.getCurrentUser && window.cbAuth.getCurrentUser().id;

/* ---------- Mappers DB ↔ UI ---------- */

const fromBusiness = (r) => r && ({
  name: r.name, owner: r.owner, initials: r.initials, hue: r.hue,
  phone: r.phone, contactEmail: r.contact_email,
  address: r.address, postalCode: r.postal_code, city: r.city,
  siret: r.siret, legalForm: r.legal_form,
  vatStatus: r.vat_status, vatNumber: r.vat_number, vatRate: Number(r.vat_rate || 0),
  iban: r.iban, paymentTermsDays: r.payment_terms_days, paymentMethods: r.payment_methods,
  bookingSlug: r.booking_slug,
  invoiceCounter: r.invoice_counter,
  avatar: r.avatar_data_url || "",
  rcsRm: r.rcs_rm || "",
  mediatorName: r.mediator_name || "",
  mediatorUrl: r.mediator_url || "",
  policyCancelHours: r.policy_cancel_hours == null ? 24 : Number(r.policy_cancel_hours),
  policyModifyHours: r.policy_modify_hours == null ? 24 : Number(r.policy_modify_hours),
  policyMaxAdvanceDays: r.policy_max_advance_days == null ? 60 : Number(r.policy_max_advance_days),
  policyBufferMin: r.policy_buffer_min == null ? 0 : Number(r.policy_buffer_min),
  policyAcompteType: r.policy_acompte_type || "none",
  policyAcompteValue: r.policy_acompte_value == null ? 30 : Number(r.policy_acompte_value),
  policyText: r.policy_text || "",
  policyConfiguredAt: r.policy_configured_at ? Date.parse(r.policy_configured_at) : null,
});
const toBusiness = (u) => {
  const p = {};
  // V2 utilise tantôt `name` tantôt `businessName` (legacy de Ma page).
  // On accepte les deux et on map vers la colonne `name` de la DB.
  if (u.name !== undefined)             p.name = u.name;
  else if (u.businessName !== undefined) p.name = u.businessName;
  if (u.owner !== undefined)            p.owner = u.owner;
  if (u.initials !== undefined)         p.initials = u.initials;
  if (u.hue !== undefined)              p.hue = u.hue;
  if (u.phone !== undefined)            p.phone = u.phone;
  if (u.contactEmail !== undefined)     p.contact_email = u.contactEmail;
  if (u.address !== undefined)          p.address = u.address;
  if (u.postalCode !== undefined)       p.postal_code = u.postalCode;
  if (u.city !== undefined)             p.city = u.city;
  if (u.siret !== undefined)            p.siret = u.siret;
  if (u.legalForm !== undefined)        p.legal_form = u.legalForm;
  if (u.vatStatus !== undefined)        p.vat_status = u.vatStatus;
  if (u.vatNumber !== undefined)        p.vat_number = u.vatNumber;
  if (u.vatRate !== undefined)          p.vat_rate = u.vatRate;
  if (u.iban !== undefined)             p.iban = u.iban;
  if (u.paymentTermsDays !== undefined) p.payment_terms_days = u.paymentTermsDays;
  if (u.paymentMethods !== undefined)   p.payment_methods = u.paymentMethods;
  if (u.bookingSlug !== undefined)      p.booking_slug = u.bookingSlug;
  if (u.invoiceCounter !== undefined)   p.invoice_counter = u.invoiceCounter;
  if (u.avatar !== undefined)           p.avatar_data_url = u.avatar;
  if (u.rcsRm !== undefined)            p.rcs_rm = u.rcsRm;
  if (u.mediatorName !== undefined)     p.mediator_name = u.mediatorName;
  if (u.mediatorUrl !== undefined)      p.mediator_url = u.mediatorUrl;
  if (u.policyCancelHours !== undefined) p.policy_cancel_hours = u.policyCancelHours;
  if (u.policyModifyHours !== undefined) p.policy_modify_hours = u.policyModifyHours;
  if (u.policyMaxAdvanceDays !== undefined) p.policy_max_advance_days = u.policyMaxAdvanceDays;
  if (u.policyBufferMin !== undefined)   p.policy_buffer_min = u.policyBufferMin;
  if (u.policyAcompteType !== undefined) p.policy_acompte_type = u.policyAcompteType;
  if (u.policyAcompteValue !== undefined) p.policy_acompte_value = u.policyAcompteValue;
  if (u.policyText !== undefined)        p.policy_text = u.policyText;
  return p;
};

const fromClient = (r) => ({
  id: r.id, name: r.name, email: r.email || "", phone: r.phone || "",
  notes: r.notes || "", birthday: r.birthday || "", social: r.social || "",
  visits: r.visits || 0, spent: Number(r.spent || 0), fid: r.fid || 0,
  last: r.last_visit || "—", tags: r.tags || [], hue: r.hue || 30,
});
const toClient = (c) => {
  const p = {};
  if (c.name !== undefined)     p.name = c.name;
  if (c.email !== undefined)    p.email = c.email;
  if (c.phone !== undefined)    p.phone = c.phone;
  if (c.notes !== undefined)    p.notes = c.notes;
  if (c.birthday !== undefined) p.birthday = c.birthday;
  if (c.social !== undefined)   p.social = c.social;
  if (c.visits !== undefined)   p.visits = c.visits;
  if (c.spent !== undefined)    p.spent = c.spent;
  if (c.fid !== undefined)      p.fid = c.fid;
  if (c.last !== undefined)     p.last_visit = c.last;
  if (c.tags !== undefined)     p.tags = c.tags;
  if (c.hue !== undefined)      p.hue = c.hue;
  return p;
};

const fromAppointment = (r) => ({
  id: r.id,
  clientId: r.client_id, serviceId: r.service_id,
  clientAccountId: r.client_account_id || null,
  day: r.day, h: Number(r.h), d: Number(r.d),
  color: r.color || "accent",
  done: !!r.done, invoiceId: r.invoice_id,
  completedAt: r.completed_at ? Date.parse(r.completed_at) : null,
  cancelledAt: r.cancelled_at ? Date.parse(r.cancelled_at) : null,
  cancelledBy: r.cancelled_by || null,
});
const toAppointment = (a) => {
  const p = {};
  if (a.clientId !== undefined)    p.client_id = a.clientId;
  if (a.serviceId !== undefined)   p.service_id = a.serviceId;
  if (a.day !== undefined)         p.day = a.day;
  if (a.h !== undefined)           p.h = a.h;
  if (a.d !== undefined)           p.d = a.d;
  if (a.color !== undefined)       p.color = a.color;
  if (a.done !== undefined)        p.done = a.done;
  if (a.invoiceId !== undefined)   p.invoice_id = a.invoiceId;
  if (a.completedAt !== undefined) p.completed_at = a.completedAt ? new Date(a.completedAt).toISOString() : null;
  return p;
};

const fromInvoice = (r) => ({
  id: r.id, number: r.number, clientId: r.client_id,
  date: r.issued_at ? _fmtDateFR(r.issued_at) : "",
  amount: Number(r.amount || 0), paid: !!r.paid, vatRate: Number(r.vat_rate || 0),
  designation: r.designation || "",
  prestationDate: r.prestation_date ? _fmtDateFR(r.prestation_date) : "",
  dueDate: r.due_date ? _fmtDateFR(r.due_date) : "",
  creditOf: r.credit_of || "",
});
const toInvoice = (i) => {
  const p = {};
  if (i.number !== undefined)         p.number = i.number;
  if (i.clientId !== undefined)       p.client_id = i.clientId;
  if (i.date !== undefined)           p.issued_at = _dateToISO(i.date);
  if (i.amount !== undefined)         p.amount = i.amount;
  if (i.paid !== undefined)           p.paid = i.paid;
  if (i.vatRate !== undefined)        p.vat_rate = i.vatRate;
  if (i.designation !== undefined)    p.designation = i.designation;
  if (i.prestationDate !== undefined) p.prestation_date = _dateToISO(i.prestationDate);
  if (i.dueDate !== undefined)        p.due_date = _dateToISO(i.dueDate);
  if (i.creditOf !== undefined)       p.credit_of = i.creditOf;
  return p;
};

const fromStock = (r) => ({
  id: r.id, name: r.name, qty: r.qty, min: r.min_qty, price: Number(r.price || 0),
});
const toStock = (s) => {
  const p = {};
  if (s.name !== undefined)  p.name = s.name;
  if (s.qty !== undefined)   p.qty = s.qty;
  if (s.min !== undefined)   p.min_qty = s.min;
  if (s.price !== undefined) p.price = s.price;
  return p;
};

/* ----- Services (prestations) ----- */
const fromService = (r) => ({
  id: r.id, name: r.name,
  price: Number(r.price || 0),
  duration: Number(r.duration || 1),
  description: r.description || "",
  active: r.active === false ? false : true,
  // V2 extras
  categoryId: r.category_id || null,
  colorId: r.color_id != null ? Number(r.color_id) : null,
  priceFrom: !!r.price_from,
  // Délai par prestation. Requiert la migration :
  //   ALTER TABLE services ADD COLUMN buffer_min int4;
  // Le RPC public get_public_booking_page doit aussi exposer ce champ.
  bufferMin: r.buffer_min != null ? Number(r.buffer_min) : null,
});
const toService = (s) => {
  const p = {};
  if (s.name !== undefined)        p.name = s.name;
  if (s.price !== undefined)       p.price = s.price;
  if (s.duration !== undefined)    p.duration = s.duration;
  if (s.description !== undefined) p.description = s.description;
  if (s.active !== undefined)      p.active = s.active;
  if (s.categoryId !== undefined)  p.category_id = s.categoryId;
  if (s.colorId !== undefined)     p.color_id    = s.colorId;
  if (s.priceFrom !== undefined)   p.price_from  = !!s.priceFrom;
  // Délai (battement) par prestation — colonne ajoutée par migration SQL.
  if (s.bufferMin !== undefined) p.buffer_min = s.bufferMin;
  return p;
};

/* ----- Service categories (V2) ----- */
const fromServiceCategory = (r) => ({
  id: r.id,
  name: r.name || "",
  description: r.description || "",
  colorId: r.color_id != null ? Number(r.color_id) : null,
  order: Number(r.order || 0),
});
const toServiceCategory = (c) => {
  const p = {};
  if (c.name !== undefined)        p.name = c.name;
  if (c.description !== undefined) p.description = c.description;
  if (c.colorId !== undefined)     p.color_id = c.colorId;
  if (c.order !== undefined)       p.order = c.order;
  return p;
};

/* ----- Gift cards (V2) ----- */
const fromGiftCard = (r) => ({
  id: r.id,
  code: r.code,
  amount: Number(r.amount || 0),
  recipientName: r.recipient_name || "",
  recipientEmail: r.recipient_email || "",
  senderName: r.sender_name || "",
  message: r.message || "",
  createdAt: r.created_at ? new Date(r.created_at).toISOString().slice(0, 10) : "",
  expiresAt: r.expires_at || "",
  used: !!r.used,
  usedAt: r.used_at || "",
});
const toGiftCard = (g) => {
  const p = {};
  if (g.code !== undefined)           p.code = g.code;
  if (g.amount !== undefined)         p.amount = g.amount;
  if (g.recipientName !== undefined)  p.recipient_name  = g.recipientName;
  if (g.recipientEmail !== undefined) p.recipient_email = g.recipientEmail;
  if (g.senderName !== undefined)     p.sender_name     = g.senderName;
  if (g.message !== undefined)        p.message         = g.message;
  if (g.expiresAt !== undefined)      p.expires_at      = g.expiresAt || null;
  if (g.used !== undefined)           p.used            = !!g.used;
  if (g.usedAt !== undefined)         p.used_at         = g.usedAt || null;
  return p;
};

const fromNotif = (r) => ({
  id: r.id, type: r.type, title: r.title, message: r.message,
  createdAt: r.created_at ? Date.parse(r.created_at) : Date.now(),
  read: !!r.read, clientId: r.client_id, appointmentId: r.appointment_id,
});

const fromVacation = (r) => ({
  id: r.id, start: r.starts_on, end: r.ends_on, reason: r.reason || "Fermeture",
});

const fromBookingSettings = (r) => r && ({
  enabled: r.enabled,
  slotDuration: r.slot_duration,
  leadTimeMinutes: r.lead_time_minutes,
  schedule: r.schedule || {},
  preferredSocial: r.preferred_social || "instagram",
  // Personnalisation Ma page (thème, bannière, bio, tagline, tags, avatarShape…)
  pageCustom: r.page_custom || null,
});

const _fmtDateFR = (isoDate) => {
  try {
    const d = new Date(isoDate + "T00:00:00");
    return d.toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" });
  } catch { return isoDate; }
};
const _dateToISO = (d) => {
  // Already ISO ?
  if (/^\d{4}-\d{2}-\d{2}/.test(d)) return d.slice(0, 10);
  // Else keep today
  return new Date().toISOString().slice(0, 10);
};

/* ---------- API publique ---------- */

const cbCloud = {
  /** true si on a un client Supabase + un user connecté */
  isActive() { return !!(_sb() && _uid()); },

  async loadAll() {
    if (!cbCloud.isActive()) return null;
    const sb = _sb();
    // Promise.allSettled au lieu de all : si une table n'existe pas (ex :
    // service_categories pas encore créée car migration 012 pas appliquée),
    // les autres queries complètent quand même et on évite le crash global
    // de loadAll qui empêche l'espace pro de se charger.
    const results = await Promise.allSettled([
      sb.from("businesses").select("*").single(),
      sb.from("clients").select("*").order("created_at", { ascending: false }),
      sb.from("appointments").select("*"),
      sb.from("invoices").select("*").order("created_at", { ascending: false }),
      sb.from("stock_items").select("*").order("created_at", { ascending: true }),
      sb.from("notifications").select("*").order("created_at", { ascending: false }).limit(50),
      sb.from("booking_settings").select("*").single(),
      sb.from("vacations").select("*").order("starts_on"),
      sb.from("services").select("*").order("created_at", { ascending: false }),
      sb.from("service_categories").select("*").order("order", { ascending: true }),
      sb.from("gift_cards").select("*").order("created_at", { ascending: false }),
    ]);
    // Helper : extrait {data, error} d'un settled result, retourne valeur
    // safe ({data: null, error}) si la promesse a rejeté.
    const settle = (r, name) => {
      if (r.status === "rejected") {
        console.warn(`[cbCloud] ${name}: rejected`, r.reason && r.reason.message);
        return { data: null, error: r.reason };
      }
      return r.value || { data: null, error: null };
    };
    const [biz, cl, ap, inv, st, nt, bs, vac, sv, sc, gc] = [
      settle(results[0],  "businesses"),
      settle(results[1],  "clients"),
      settle(results[2],  "appointments"),
      settle(results[3],  "invoices"),
      settle(results[4],  "stock_items"),
      settle(results[5],  "notifications"),
      settle(results[6],  "booking_settings"),
      settle(results[7],  "vacations"),
      settle(results[8],  "services"),
      settle(results[9],  "service_categories"),
      settle(results[10], "gift_cards"),
    ];

    if (biz.error) window.cbDebug && window.cbDebug.warn("[cbCloud] businesses:", biz.error.message);
    if (cl.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] clients:", cl.error.message);
    if (ap.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] appointments:", ap.error.message);
    if (inv.error) window.cbDebug && window.cbDebug.warn("[cbCloud] invoices:", inv.error.message);
    if (st.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] stock:", st.error.message);
    if (nt.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] notifications:", nt.error.message);
    if (bs.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] booking_settings:", bs.error.message);
    if (vac.error) window.cbDebug && window.cbDebug.warn("[cbCloud] vacations:", vac.error.message);
    if (sv.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] services:", sv.error.message);
    if (sc.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] service_categories:", sc.error.message);
    if (gc.error)  window.cbDebug && window.cbDebug.warn("[cbCloud] gift_cards:", gc.error.message);

    const business = fromBusiness(biz.data) || {};
    // Compat : Ma page lit data.business.businessName, on l'aliase sur name.
    if (business.name && !business.businessName) business.businessName = business.name;
    const bookingSettings = {
      ...(fromBookingSettings(bs.data) || { enabled: true, slotDuration: 30, leadTimeMinutes: 120, schedule: {}, pageCustom: null }),
      vacations: (vac.data || []).map(fromVacation),
      activeServiceIds: [], // legacy
      autoConfirm: true,
    };

    return {
      business,
      clients:           (cl.data  || []).map(fromClient),
      appointments:      (ap.data  || []).map(fromAppointment),
      invoices:          (inv.data || []).map(fromInvoice),
      stock:             (st.data  || []).map(fromStock),
      notifications:     (nt.data  || []).map(fromNotif),
      services:          (sv.data  || []).map(fromService),
      serviceCategories: (sc.data  || []).map(fromServiceCategory),
      giftCards:         (gc.data  || []).map(fromGiftCard),
      bookingSettings,
      fideliteRules: {
        visits:      biz.data ? biz.data.fidelity_visits : 10,
        rewardLabel: biz.data ? biz.data.fidelity_reward : "1 prestation offerte",
      },
      // Local-only (plus tard)
      messages: {}, conversations: [], activeConv: null, promos: [],
      invoiceCounter: biz.data ? (biz.data.invoice_counter || 1) : 1,
    };
  },

  /* ----- Realtime, push instantané des changements DB → UI ----- */
  /* Renvoie une fonction de désabonnement. Les handlers reçoivent soit
     { row: <objet déjà mappé> } pour INSERT/UPDATE, soit { deleteId } pour
     DELETE. Le front fait un upsert/filter par id côté state. */
  subscribe(handlers) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return () => {};

    const filter = `user_id=eq.${uid}`;
    const channel = sb.channel(`cb-rt-${uid}-${Date.now()}`);

    const listen = (table, mapper, handlerName) => {
      channel.on(
        "postgres_changes",
        { event: "*", schema: "public", table, filter },
        (payload) => {
          const fn = handlers[handlerName];
          if (!fn) return;
          if (payload.eventType === "DELETE") {
            fn({ deleteId: payload.old && payload.old.id });
          } else {
            fn({ row: mapper(payload.new) });
          }
        }
      );
    };

    listen("appointments",     fromAppointment,     "onAppointment");
    listen("clients",          fromClient,          "onClient");
    listen("invoices",         fromInvoice,         "onInvoice");
    listen("services",         fromService,         "onService");
    listen("stock_items",      fromStock,           "onStock");
    listen("notifications",    fromNotif,           "onNotification");
    listen("vacations",        fromVacation,        "onVacation");
    // booking_settings + businesses : intentionnellement PAS écoutés en
    // realtime. Sans REPLICA IDENTITY FULL côté Postgres, les UPDATE
    // events envoient une row partielle (= seulement les colonnes
    // modifiées). Notre handler ferait alors un merge avec des champs
    // undefined qui ÉCRASENT les valeurs locales correctes (page_custom
    // notamment), l'utilisateur voit ses modifs disparaitre.
    //
    // L'optimistic update local (setData dans actions) suffit pour que
    // la preview reflète les modifs instantanément, et useAppData fait
    // un loadAll() au mount du dashboard pour les données fraîches.
    // Si tu veux activer la sync cross-device, il faudra d'abord :
    //   alter table public.booking_settings replica identity full;
    //   alter table public.businesses        replica identity full;

    channel.subscribe();
    return () => { try { sb.removeChannel(channel); } catch {} };
  },

  /* ----- Compte (RGPD) ----- */
  async resetMyData() {
    const sb = _sb(); if (!sb) return;
    const { error } = await sb.rpc("cb_reset_my_data");
    if (error) throw error;
  },
  async deleteMyAccount() {
    const sb = _sb(); if (!sb) return;
    const { error } = await sb.rpc("cb_delete_my_account");
    if (error) throw error;
  },

  /* ----- Services ----- */
  async upsertService(s, isInsert) {
    const sb = _sb(); if (!sb) return;
    const uid = _uid(); if (!uid) return;
    if (isInsert) {
      const { data, error } = await sb.from("services")
        .insert({ ...toService(s), user_id: uid, id: s.id }).select().single();
      if (error) throw error; return fromService(data);
    }
    const { error } = await sb.from("services").update(toService(s)).eq("id", s.id);
    if (error) throw error;
  },
  async removeService(id) {
    const { error } = await _sb().from("services").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Clients ----- */
  async upsertClient(c, isInsert) {
    const sb = _sb(); if (!sb) return;
    const uid = _uid(); if (!uid) return;
    if (isInsert) {
      const { data, error } = await sb.from("clients").insert({ ...toClient(c), user_id: uid, id: c.id }).select().single();
      if (error) throw error; return fromClient(data);
    }
    const { error } = await sb.from("clients").update(toClient(c)).eq("id", c.id);
    if (error) throw error;
  },
  async removeClient(id) {
    const { error } = await _sb().from("clients").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Appointments ----- */
  async upsertAppointment(a, isInsert) {
    const sb = _sb(); if (!sb) return;
    const uid = _uid(); if (!uid) return;
    if (isInsert) {
      const { data, error } = await sb.from("appointments").insert({ ...toAppointment(a), user_id: uid, id: a.id }).select().single();
      if (error) throw error; return fromAppointment(data);
    }
    const { error } = await sb.from("appointments").update(toAppointment(a)).eq("id", a.id);
    if (error) throw error;
  },
  async removeAppointment(id) {
    const { error } = await _sb().from("appointments").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Invoices ----- */
  async upsertInvoice(i, isInsert) {
    const sb = _sb(); if (!sb) return;
    const uid = _uid(); if (!uid) return;
    if (isInsert) {
      const { data, error } = await sb.from("invoices").insert({ ...toInvoice(i), user_id: uid, id: i.id }).select().single();
      if (error) throw error; return fromInvoice(data);
    }
    const { error } = await sb.from("invoices").update(toInvoice(i)).eq("id", i.id);
    if (error) throw error;
  },
  async removeInvoice(id) {
    const { error } = await _sb().from("invoices").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Stock ----- */
  async upsertStock(s, isInsert) {
    const sb = _sb(); if (!sb) return;
    const uid = _uid(); if (!uid) return;
    if (isInsert) {
      const { data, error } = await sb.from("stock_items").insert({ ...toStock(s), user_id: uid, id: s.id }).select().single();
      if (error) throw error; return fromStock(data);
    }
    const { error } = await sb.from("stock_items").update(toStock(s)).eq("id", s.id);
    if (error) throw error;
  },
  async removeStock(id) {
    const { error } = await _sb().from("stock_items").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Business (paramètres + compteur de facture + règles fidélité) ----- */
  async updateBusiness(patch) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return;
    const { error } = await sb.from("businesses").update(toBusiness(patch)).eq("user_id", uid);
    if (error) throw error;
  },
  // Acomptes : on lit/écrit directement la table acompte_settings.
  // Pas de mapper séparé, structure simple key→value identique côté UI.
  async getAcompteSettings() {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return null;
    const { data, error } = await sb.from("acompte_settings").select("*").eq("user_id", uid).maybeSingle();
    if (error) throw error;
    if (!data) return null;
    return {
      enabled: !!data.enabled,
      mode: data.mode || "percent",          // 'percent' | 'fixed'
      percent: Number(data.percent || 30),
      minAmount: Number(data.min_amount || 50),
      policyText: data.policy_text || "",
      refundWindowHrs: Number(data.refund_window_hrs || 48),
      paymentProvider: data.payment_provider || "",
      paymentLink: data.payment_link || "",
      paymentInstructions: data.payment_instructions || "",
    };
  },
  async upsertAcompteSettings(patch) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return;
    const p = { user_id: uid, updated_at: new Date().toISOString() };
    if (patch.enabled !== undefined)             p.enabled = patch.enabled;
    if (patch.mode !== undefined)                p.mode = patch.mode;
    if (patch.percent !== undefined)             p.percent = patch.percent;
    if (patch.minAmount !== undefined)           p.min_amount = patch.minAmount;
    if (patch.policyText !== undefined)          p.policy_text = patch.policyText;
    if (patch.refundWindowHrs !== undefined)     p.refund_window_hrs = patch.refundWindowHrs;
    if (patch.paymentProvider !== undefined)     p.payment_provider = patch.paymentProvider;
    if (patch.paymentLink !== undefined)         p.payment_link = patch.paymentLink;
    if (patch.paymentInstructions !== undefined) p.payment_instructions = patch.paymentInstructions;
    const { error } = await sb.from("acompte_settings").upsert(p, { onConflict: "user_id" });
    // Si la colonne 'mode' n'existe pas encore (migration 022 non appliquée),
    // on retire le champ et on ré-essaye sans, pour ne pas casser le save.
    if (error && error.code === "PGRST204" && p.mode !== undefined) {
      delete p.mode;
      const { error: err2 } = await sb.from("acompte_settings").upsert(p, { onConflict: "user_id" });
      if (err2) throw err2;
      return;
    }
    if (error) throw error;
  },
  // Récupère TOUS les RDV du client connecté (auth.uid()) groupés par pro,
  // dans la structure attendue par EspacePage (cf. _DEMO_CLIENT.pros).
  // Renvoie [] si pas de session ou si la RPC manque (migration 025 pas
  // appliquée). Le client espace tombe alors sur une liste vide propre
  // ("Aucun RDV chez vos pros pour l'instant").
  async getMyAppointments() {
    const sb = _sb(); if (!sb) return [];
    try {
      const { data, error } = await sb.rpc("get_my_appointments");
      if (error) {
        if (error.code === "PGRST202") {
          window.cbDebug && window.cbDebug.warn("[getMyAppointments] RPC manquante — applique migration 025");
          return [];
        }
        throw error;
      }
      return Array.isArray(data) ? data : (data || []);
    } catch (e) {
      window.cbDebug && window.cbDebug.warn("[getMyAppointments]", e);
      return [];
    }
  },

  // Rattache au compte client connecté tous ses RDV "orphelins" pris en
  // mode signup (où auth.uid() était null au moment du RPC). Match par email
  // de l'utilisateur vs email du client lié au RDV. Retourne le nombre de
  // RDV rattachés. Idempotent — peut être appelée à chaque login.
  async linkMyOrphanAppointments() {
    const sb = _sb(); if (!sb) return 0;
    try {
      const { data, error } = await sb.rpc("link_my_orphan_appointments");
      if (error) {
        // Si la fonction n'existe pas encore (migration 023 pas appliquée),
        // on retourne 0 sans planter.
        if (error.code === "PGRST202") return 0;
        throw error;
      }
      return Number(data || 0);
    } catch (e) {
      window.cbDebug && window.cbDebug.warn("[linkMyOrphanAppointments]", e);
      return 0;
    }
  },

  // Marque la politique comme validée par le pro (gate pour activer la
  // page publique de RDV). Essaye d'abord le RPC (créé par migration 020) ;
  // si la fonction n'existe pas (cache PostgREST pas refresh, ou migration
  // pas encore appliquée), retombe sur un UPDATE direct sur businesses
  // qui marche grâce aux RLS user_id = auth.uid().
  async markPolicyConfigured() {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return null;
    try {
      const { data, error } = await sb.rpc("mark_policy_configured");
      if (!error) return data ? Date.parse(data) : Date.now();
      // PGRST202 = function not found → fallback. Pour toute autre erreur on raise.
      if (error.code && error.code !== "PGRST202") throw error;
    } catch (e) {
      if (e && e.code && e.code !== "PGRST202") throw e;
    }
    const nowIso = new Date().toISOString();
    const { error: upErr } = await sb.from("businesses")
      .update({ policy_configured_at: nowIso })
      .eq("user_id", uid);
    if (upErr) throw upErr;
    return Date.now();
  },
  async setFideliteRules(rules) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return;
    const p = {};
    if (rules.visits !== undefined)      p.fidelity_visits = rules.visits;
    if (rules.rewardLabel !== undefined) p.fidelity_reward = rules.rewardLabel;
    const { error } = await sb.from("businesses").update(p).eq("user_id", uid);
    if (error) throw error;
  },
  async incrementInvoiceCounter(next) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return;
    const { error } = await sb.from("businesses").update({ invoice_counter: next }).eq("user_id", uid);
    if (error) throw error;
  },

  /* ----- Booking settings ----- */
  async updateBookingSettings(patch) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return;
    const p = {};
    if (patch.enabled !== undefined)         p.enabled = patch.enabled;
    if (patch.slotDuration !== undefined)    p.slot_duration = patch.slotDuration;
    if (patch.leadTimeMinutes !== undefined) p.lead_time_minutes = patch.leadTimeMinutes;
    if (patch.schedule !== undefined)        p.schedule = patch.schedule;
    if (patch.preferredSocial !== undefined) p.preferred_social = patch.preferredSocial;
    // Ma page : conteneur JSON entier. On l'envoie tel quel, la DB stocke en JSONB.
    if (patch.pageCustom !== undefined)      p.page_custom = patch.pageCustom;
    if (Object.keys(p).length === 0) return;
    // upsert : si la ligne n'existe pas encore (cas rare)
    const { error } = await sb.from("booking_settings").upsert({ user_id: uid, ...p }, { onConflict: "user_id" });
    if (error) throw error;
  },

  /* ----- Vacations ----- */
  async addVacation(v) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return v;
    const { data, error } = await sb.from("vacations")
      .insert({ user_id: uid, starts_on: v.start, ends_on: v.end, reason: v.reason || "Fermeture" })
      .select().single();
    if (error) throw error;
    return fromVacation(data);
  },
  async removeVacation(id) {
    const { error } = await _sb().from("vacations").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Service categories (V2) ----- */
  async upsertServiceCategory(c, isInsert) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return c;
    if (isInsert) {
      const { data, error } = await sb.from("service_categories")
        .insert({ id: c.id, user_id: uid, ...toServiceCategory(c) })
        .select().single();
      if (error) throw error;
      return fromServiceCategory(data);
    } else {
      const { error } = await sb.from("service_categories").update(toServiceCategory(c)).eq("id", c.id);
      if (error) throw error;
      return c;
    }
  },
  async removeServiceCategory(id) {
    const { error } = await _sb().from("service_categories").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Gift cards (V2) ----- */
  async upsertGiftCard(g, isInsert) {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return g;
    if (isInsert) {
      const { data, error } = await sb.from("gift_cards")
        .insert({ id: g.id, user_id: uid, ...toGiftCard(g) })
        .select().single();
      if (error) throw error;
      return fromGiftCard(data);
    } else {
      const { error } = await sb.from("gift_cards").update(toGiftCard(g)).eq("id", g.id);
      if (error) throw error;
      return g;
    }
  },
  async removeGiftCard(id) {
    const { error } = await _sb().from("gift_cards").delete().eq("id", id);
    if (error) throw error;
  },

  /* ----- Notifications ----- */
  async markAllNotificationsRead() {
    const sb = _sb(); const uid = _uid();
    if (!sb || !uid) return;
    const { error } = await sb.from("notifications").update({ read: true }).eq("user_id", uid);
    if (error) throw error;
  },
  async removeNotification(id) {
    const { error } = await _sb().from("notifications").delete().eq("id", id);
    if (error) throw error;
  },
};

window.cbCloud = cbCloud;
