/* ClientBase — App data layer, modal, toasts, shared form primitives */

const CB_LS_KEY = "cb_app_data_v3";
const CB_USER_KEY = "cb_user_v1";
const CB_COOKIE_KEY = "cb_cookie_consent_v1";

const uid = () => Math.random().toString(36).slice(2, 10);

const slugify = (str) => (str || "")
  .toLowerCase()
  .normalize("NFD")
  .replace(/[\u0300-\u036f]/g, "")
  .replace(/[^a-z0-9]+/g, "-")
  .replace(/^-+|-+$/g, "")
  .slice(0, 28);

const generateBookingSlug = (businessName) => {
  const base = slugify(businessName) || "pro";
  return `${base}-${Math.random().toString(36).slice(2, 6)}`;
};

/* ===== Auth — Supabase si dispo, sinon fallback localStorage ===== */
const _sha256 = async (str) => {
  const buf = new TextEncoder().encode(str);
  const hash = await crypto.subtle.digest("SHA-256", buf);
  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, "0")).join("");
};

const _authListeners = new Set();
const _notifyAuth = () => _authListeners.forEach(fn => { try { fn(cbAuth.getCurrentUser()); } catch (e) { console.error(e); } });

// Cache du user courant (lu de façon synchrone par l'UI).
// Hydraté depuis localStorage au démarrage, rafraîchi depuis Supabase si connecté.
let _currentUser = (() => {
  try { return JSON.parse(localStorage.getItem(CB_USER_KEY) || "null"); }
  catch { return null; }
})();
const _setCurrentUser = (u) => {
  _currentUser = u;
  if (u) localStorage.setItem(CB_USER_KEY, JSON.stringify(u));
  else   localStorage.removeItem(CB_USER_KEY);
  _notifyAuth();
};

// Lit la ligne `businesses` de l'utilisateur et fusionne les infos
// (businessName, ownerName, bookingSlug) dans le cache. Fait aussi la
// création d'urgence si le trigger a raté.
const _hydrateFromSupabase = async (session) => {
  if (!session || !window.cbSupabase) return null;
  const sb = window.cbSupabase;
  const u  = session.user;

  let { data: biz } = await sb
    .from("businesses")
    .select("name, owner, booking_slug")
    .eq("user_id", u.id)
    .maybeSingle();

  // Si le trigger n'a pas encore provisionné la ligne (signup first-time), on l'attend.
  if (!biz) {
    await new Promise(r => setTimeout(r, 400));
    const retry = await sb.from("businesses").select("name, owner, booking_slug").eq("user_id", u.id).maybeSingle();
    biz = retry.data;
  }

  const meta = u.user_metadata || {};
  const user = {
    id:              u.id,
    email:           u.email,
    emailVerified:   !!u.email_confirmed_at,
    businessName:    (biz && biz.name)  || meta.business_name || "Mon activité",
    ownerName:       (biz && biz.owner) || meta.owner_name    || (u.email || "").split("@")[0],
    bookingSlug:     (biz && biz.booking_slug) || generateBookingSlug(meta.business_name || "pro"),
    createdAt:       u.created_at ? Date.parse(u.created_at) : Date.now(),
  };
  _setCurrentUser(user);
  return user;
};

// Hydratation initiale
if (window.cbSupabase) {
  window.cbSupabase.auth.getSession().then(({ data }) => {
    if (data.session) _hydrateFromSupabase(data.session);
    else if (_currentUser && !_currentUser.id) {
      // On a un user localStorage hérité de la bêta — on le laisse tel quel
      // jusqu'à ce qu'il se reconnecte. L'étape 6 proposera la migration.
    }
  });
  window.cbSupabase.auth.onAuthStateChange((event, session) => {
    if (event === "SIGNED_OUT" || !session) {
      _setCurrentUser(null);
    } else {
      _hydrateFromSupabase(session);
    }
  });
}

/* ---- Validations partagées (email + mot de passe) ---- */
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/;
const isValidEmail = (s) => EMAIL_RE.test((s || "").trim());

// Robuste sans être décourageant : 8+ caractères, dont au moins 1 lettre et 1 chiffre.
const validatePassword = (p) => {
  if (!p || p.length < 8)   return "Mot de passe trop court (8 caractères minimum).";
  if (!/[A-Za-z]/.test(p))  return "Le mot de passe doit contenir au moins une lettre.";
  if (!/[0-9]/.test(p))     return "Le mot de passe doit contenir au moins un chiffre.";
  return null;
};

// Expose pour les composants UI (barre de force).
const scorePassword = (p) => {
  if (!p) return 0;
  let s = 0;
  if (p.length >= 8)        s++;
  if (p.length >= 12)       s++;
  if (/[A-Z]/.test(p))      s++;
  if (/[0-9]/.test(p))      s++;
  if (/[^A-Za-z0-9]/.test(p)) s++;
  return Math.min(4, s);
};

const cbAuth = {
  getCurrentUser: () => _currentUser,
  isValidEmail,
  validatePassword,
  scorePassword,

  signup: async ({ email, password, businessName, ownerName }) => {
    email = (email || "").trim().toLowerCase();
    if (!isValidEmail(email)) return { error: "Email invalide. Exemple : prenom@exemple.fr" };
    const pwErr = validatePassword(password);
    if (pwErr) return { error: pwErr };
    if (!businessName || !businessName.trim()) return { error: "Nom de l'activité requis." };
    if (!ownerName || !ownerName.trim()) return { error: "Votre nom est requis." };

    if (window.cbSupabase) {
      const { data, error } = await window.cbSupabase.auth.signUp({
        email, password,
        options: {
          data: {
            business_name: businessName.trim(),
            owner_name: ownerName.trim(),
          },
        },
      });
      if (error) return { error: _prettifyAuthError(error.message) };
      // Quand "Confirm email" est OFF dans Supabase (recommandé), on a
      // une session immédiate → utilisateur connecté direct.
      if (data.session) {
        const user = await _hydrateFromSupabase(data.session);
        return { ok: true, user };
      }
      // Sinon, ça veut dire que "Confirm email" est ON côté Supabase.
      // On informe sans bloquer.
      return { ok: true, user: null, needsEmailConfirm: true };
    }

    // Fallback localStorage (pas de Supabase)
    const passHash = await _sha256(password);
    const user = {
      email, passHash,
      businessName: businessName.trim(),
      ownerName: ownerName.trim(),
      bookingSlug: generateBookingSlug(businessName),
      createdAt: Date.now(),
    };
    _setCurrentUser(user);
    return { ok: true, user };
  },

  login: async ({ email, password }) => {
    email = (email || "").trim().toLowerCase();
    if (!email || !password) return { error: "Email et mot de passe requis." };
    if (!isValidEmail(email)) return { error: "Email invalide. Exemple : prenom@exemple.fr" };

    if (window.cbSupabase) {
      const { data, error } = await window.cbSupabase.auth.signInWithPassword({ email, password });
      if (error) return { error: _prettifyAuthError(error.message) };
      const user = await _hydrateFromSupabase(data.session);
      return { ok: true, user };
    }

    // Fallback localStorage
    const saved = _currentUser;
    if (!saved) return { error: "Aucun compte trouvé. Créez votre compte d'abord." };
    if (saved.email !== email) return { error: "Email ou mot de passe incorrect." };
    const passHash = await _sha256(password);
    if (saved.passHash !== passHash) return { error: "Email ou mot de passe incorrect." };
    _notifyAuth();
    return { ok: true, user: saved };
  },

  logout: async () => {
    if (window.cbSupabase) {
      try { await window.cbSupabase.auth.signOut(); } catch (e) { console.error(e); }
    }
    _setCurrentUser(null);
  },

  ensureBookingSlug: () => {
    const u = _currentUser;
    if (!u) return null;
    if (u.bookingSlug) return u.bookingSlug;
    u.bookingSlug = generateBookingSlug(u.businessName);
    _setCurrentUser({ ...u });
    return u.bookingSlug;
  },

  updateBookingSlug: (newSlug) => {
    const u = _currentUser;
    if (!u) return null;
    const clean = slugify(newSlug);
    if (!clean) return u.bookingSlug;

    if (window.cbSupabase && u.id) {
      window.cbSupabase
        .from("businesses")
        .update({ booking_slug: clean })
        .eq("user_id", u.id)
        .then(({ error }) => {
          if (error && error.code === "23505") {
            // collision unique — on retombe sur l'ancien slug
            console.warn("[cbAuth] slug déjà pris :", clean);
          }
        });
    }
    _setCurrentUser({ ...u, bookingSlug: clean });
    return clean;
  },

  findUserBySlug: (slug) => {
    // Synchrone — ne regarde que le user local. La page publique de réservation
    // appellera la RPC `get_public_booking` à l'étape 5.
    const u = _currentUser;
    if (u && u.bookingSlug === slug) return u;
    return null;
  },

  onChange: (fn) => {
    _authListeners.add(fn);
    return () => _authListeners.delete(fn);
  },

  /** Envoie un email de réinitialisation. Le lien renvoie vers l'app
      avec `?reset=1#access_token=…` → détecté au boot (index.html). */
  resetPassword: async (email) => {
    email = (email || "").trim().toLowerCase();
    if (!isValidEmail(email)) return { error: "Email invalide." };
    if (!window.cbSupabase) return { error: "Fonction indisponible en mode bêta locale." };
    const origin = `${window.location.origin}${window.location.pathname}?reset=1`;
    const { error } = await window.cbSupabase.auth.resetPasswordForEmail(email, { redirectTo: origin });
    if (error) return { error: _prettifyAuthError(error.message) };
    return { ok: true };
  },

  /** Applique un nouveau mot de passe (user doit être authentifié via le lien reçu). */
  updatePassword: async (newPassword) => {
    const pwErr = validatePassword(newPassword);
    if (pwErr) return { error: pwErr };
    if (!window.cbSupabase) return { error: "Fonction indisponible en mode bêta locale." };
    const { error } = await window.cbSupabase.auth.updateUser({ password: newPassword });
    if (error) return { error: _prettifyAuthError(error.message) };
    return { ok: true };
  },

  /** Renvoie un email de confirmation d'inscription à l'utilisateur actuel. */
  resendVerification: async () => {
    if (!window.cbSupabase) return { error: "Fonction indisponible en mode bêta locale." };
    const u = _currentUser;
    if (!u || !u.email) return { error: "Aucun compte connecté." };
    const { error } = await window.cbSupabase.auth.resend({ type: "signup", email: u.email });
    if (error) return { error: _prettifyAuthError(error.message) };
    return { ok: true };
  },

  /** Change l'email du compte. Supabase envoie un email de confirmation au
      nouvel email ; le changement est effectif une fois le lien cliqué. */
  changeEmail: async (newEmail) => {
    newEmail = (newEmail || "").trim().toLowerCase();
    if (!isValidEmail(newEmail)) return { error: "Email invalide." };
    if (!window.cbSupabase) return { error: "Fonction indisponible en mode bêta locale." };
    const u = _currentUser;
    if (u && u.email === newEmail) return { error: "C'est déjà votre email actuel." };
    const { error } = await window.cbSupabase.auth.updateUser({ email: newEmail });
    if (error) return { error: _prettifyAuthError(error.message) };
    return { ok: true };
  },
};

const _prettifyAuthError = (msg) => {
  const m = (msg || "").toLowerCase();
  if (m.includes("already registered") || m.includes("user already"))
    return "Un compte existe déjà avec cet email. Connectez-vous.";
  if (m.includes("invalid login") || m.includes("invalid credentials"))
    return "Email ou mot de passe incorrect.";
  if (m.includes("email not confirmed"))
    return "Email non confirmé. Vérifiez votre boîte de réception.";
  if (m.includes("password should be") || m.includes("weak password"))
    return "Mot de passe trop faible (6 caractères minimum).";
  if (m.includes("rate limit") || m.includes("too many"))
    return "Trop de tentatives. Patientez quelques minutes.";
  if (m.includes("redirect") && m.includes("not allowed"))
    return "URL de redirection non autorisée. Contactez le support.";
  // Erreur 500 / problème SMTP → message clair plutôt que "Internal Server Error"
  if (m.includes("500") || m.includes("internal server") || m.includes("smtp"))
    return "L'envoi d'email a échoué côté serveur. Réessayez dans 2 minutes — si ça persiste, écrivez-nous à clientbase.fr@gmail.com.";
  return msg || "Une erreur est survenue.";
};

const useCurrentUser = () => {
  const [user, setUser] = React.useState(() => cbAuth.getCurrentUser());
  React.useEffect(() => cbAuth.onChange(setUser), []);
  return user;
};

/* ===== Cookie banner ===== */
const useCookieConsent = () => {
  const [consent, setConsent] = React.useState(() => localStorage.getItem(CB_COOKIE_KEY));
  const set = (val) => {
    localStorage.setItem(CB_COOKIE_KEY, val);
    setConsent(val);
  };
  return [consent, set];
};

const CookieBanner = ({ onOpenCookies }) => {
  const [consent, setConsent] = useCookieConsent();
  if (consent) return null;
  return (
    <div style={{
      position: "fixed", bottom: 16, left: 16, right: 16,
      zIndex: 120, maxWidth: 680, margin: "0 auto",
      background: "var(--surface)", border: "1px solid var(--line-strong)",
      borderRadius: 14, padding: 18, boxShadow: "var(--sh-3)",
      display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap",
    }}>
      <div style={{
        width: 36, height: 36, borderRadius: 10,
        background: "var(--accent-soft)", color: "var(--accent-ink)",
        display: "flex", alignItems: "center", justifyContent: "center",
        flexShrink: 0,
      }}>
        <Icon name="shield" size={18}/>
      </div>
      <div style={{ flex: 1, minWidth: 220, fontSize: 13.5, color: "var(--ink-2)", lineHeight: 1.5 }}>
        <strong style={{ color: "var(--ink)" }}>On respecte votre vie privée.</strong> On utilise uniquement des cookies techniques nécessaires au fonctionnement du site. Aucun tracker, aucune pub.{" "}
        {onOpenCookies && (
          <a href="#" onClick={(e) => { e.preventDefault(); onOpenCookies(); }} style={{ color: "var(--accent-ink)", fontWeight: 520 }}>
            En savoir plus
          </a>
        )}
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        <button className="btn btn-ghost btn-sm" onClick={() => setConsent("essential")}>
          Essentiels uniquement
        </button>
        <button className="btn btn-accent btn-sm" onClick={() => setConsent("all")}>
          Tout accepter
        </button>
      </div>
    </div>
  );
};

const SEED_DATA = {
  business: {
    name: "Ongles by Léa", owner: "Léa Bernard", initials: "LB", hue: 30,
    // Contact
    phone: "",
    contactEmail: "",
    // Legal info (needed on invoices — French norms)
    address: "",
    postalCode: "",
    city: "",
    siret: "",
    legalForm: "Auto-entrepreneur",
    vatStatus: "franchise",        // "franchise" = exempt (art. 293B), "applicable" = VAT charged
    vatNumber: "",
    vatRate: 20,                    // applied only if vatStatus === "applicable"
    iban: "",
    paymentTermsDays: 0,            // 0 = à réception
    paymentMethods: "Virement, espèces, carte",
    rcsRm: "",                      // Immatriculation RCS (commerçant) ou RM (artisan)
    mediatorName: "",               // Médiateur de la consommation (art. L612-1)
    mediatorUrl: "",
  },
  services: [
    { id: "s1", name: "Gel semi-permanent", price: 45, duration: 1 },
    { id: "s2", name: "Pose complète", price: 60, duration: 1.5 },
    { id: "s3", name: "Remplissage", price: 38, duration: 1.25 },
    { id: "s4", name: "Nail-art", price: 25, duration: 0.5 },
    { id: "s5", name: "Dépose", price: 20, duration: 0.5 },
    { id: "s6", name: "Dépose + pose", price: 72, duration: 1.25 },
  ],
  clients: [
    { id: "c1", name: "Léa Morel", email: "lea.m@gmail.com", phone: "06 12 34 56 78", visits: 14, spent: 582, fid: 8, last: "il y a 3 sem.", tags: ["VIP", "Fidèle"], hue: 30, notes: "Préfère les couleurs nude. Allergie à la base Gel X." },
    { id: "c2", name: "Camille Petit", email: "camille.p@free.fr", phone: "06 98 76 54 32", visits: 8, spent: 340, fid: 6, last: "il y a 2 sem.", tags: ["Fidèle"], hue: 330, notes: "" },
    { id: "c3", name: "Inès Dubois", email: "ines.dubois@outlook.fr", phone: "06 11 22 33 44", visits: 22, spent: 892, fid: 10, last: "hier", tags: ["VIP"], hue: 220, notes: "" },
    { id: "c4", name: "Sarah Ben Ali", email: "s.benali@gmail.com", phone: "", visits: 5, spent: 215, fid: 4, last: "il y a 1 sem.", tags: ["Nouvelle"], hue: 180, notes: "" },
    { id: "c5", name: "Emma Vidal", email: "emma.v@gmail.com", phone: "", visits: 3, spent: 128, fid: 2, last: "il y a 2 mois", tags: ["Inactive"], hue: 280, notes: "" },
    { id: "c6", name: "Julie Thomas", email: "julie.t@hotmail.fr", phone: "", visits: 11, spent: 456, fid: 7, last: "il y a 4 jours", tags: ["Fidèle"], hue: 60, notes: "" },
    { id: "c7", name: "Marine Leroy", email: "marine.l@gmail.com", phone: "", visits: 6, spent: 258, fid: 5, last: "il y a 1 sem.", tags: [], hue: 140, notes: "" },
  ],
  // Données démo mappées sur la semaine en cours (lundi = jour 0 relatif).
  // Les vrais offsets sont calculés au boot (window.cbMondayOffset peut être
  // absent si shared.jsx n'a pas encore chargé — on retombe sur 0 par défaut).
  appointments: (() => {
    const mon = (typeof window !== "undefined" && window.cbMondayOffset) ? window.cbMondayOffset() : 0;
    const D = (i) => mon + i;
    return [
      { id: "a1",  day: D(0), h: 10,   d: 1,    clientId: "c5", serviceId: "s4", color: "accent" },
      { id: "a2",  day: D(0), h: 14,   d: 2,    clientId: "c6", serviceId: "s2", color: "sage" },
      { id: "a3",  day: D(1), h: 9,    d: 1,    clientId: "c1", serviceId: "s1", color: "accent" },
      { id: "a4",  day: D(1), h: 11,   d: 1.5,  clientId: "c2", serviceId: "s2", color: "sage" },
      { id: "a5",  day: D(1), h: 14,   d: 1.25, clientId: "c3", serviceId: "s3", color: "warn" },
      { id: "a6",  day: D(1), h: 15.75,d: 1.25, clientId: "c4", serviceId: "s6", color: "accent" },
      { id: "a7",  day: D(2), h: 9.5,  d: 1.5,  clientId: "c7", serviceId: "s1", color: "accent" },
      { id: "a8",  day: D(2), h: 13,   d: 1,    clientId: "c6", serviceId: "s4", color: "warn" },
      { id: "a9",  day: D(3), h: 10,   d: 1.5,  clientId: "c3", serviceId: "s3", color: "accent" },
      { id: "a10", day: D(4), h: 11,   d: 2,    clientId: "c5", serviceId: "s4", color: "sage" },
      { id: "a11", day: D(4), h: 14.5, d: 1.25, clientId: "c7", serviceId: "s5", color: "warn" },
      { id: "a12", day: D(5), h: 10,   d: 1.5,  clientId: "c2", serviceId: "s2", color: "accent" },
      { id: "a13", day: D(5), h: 13,   d: 1,    clientId: "c4", serviceId: "s1", color: "sage" },
    ];
  })(),
  messages: {
    "c1": [
      { id: "m1",  from: "them", text: "Bonjour Léa ! Je voulais confirmer pour demain 9h, c'est bien ça ?", time: "10:38" },
      { id: "m2",  from: "me",   text: "Bonjour Léa, oui tout est bien bloqué. Pose gel semi-permanent comme la dernière fois ?", time: "10:40" },
      { id: "m3",  from: "them", text: "Oui, et cette fois je tente un nude rosé si vous avez 🤭", time: "10:41" },
      { id: "m4",  from: "me",   text: "J'ai plusieurs teintes, on choisira ensemble. Hâte de vous voir !", time: "10:42" },
      { id: "m5",  from: "them", text: "Parfait, à demain 9h !", time: "10:42" },
    ],
    "c3": [
      { id: "m6",  from: "them", text: "Bonjour, possible de décaler mon RDV de vendredi à samedi ?", time: "09:14" },
      { id: "m7",  from: "them", text: "Merci d'avance !", time: "09:14" },
    ],
    "c2": [
      { id: "m8",  from: "them", text: "Merci beaucoup 🙏", time: "hier" },
    ],
    "c4": [
      { id: "m9",  from: "them", text: "Vous faites les ongles en acrygel ?", time: "hier" },
    ],
    "c5": [
      { id: "m10", from: "them", text: "Super, je prends !", time: "lun." },
    ],
  },
  conversations: [
    { clientId: "c1", last: "Parfait, à demain 9h !",          time: "10:42", unread: 0 },
    { clientId: "c3", last: "Bonjour, possible de décaler…",   time: "09:14", unread: 2 },
    { clientId: "c2", last: "Merci beaucoup 🙏",                time: "hier",  unread: 0 },
    { clientId: "c4", last: "Vous faites les ongles en…",      time: "hier",  unread: 1 },
    { clientId: "c5", last: "Super, je prends !",              time: "lun.",  unread: 0 },
  ],
  activeConv: "c1",
  invoices: [
    { id: "F-2026-0142", clientId: "c1", date: "21 avril 2026", amount: 45, paid: true },
    { id: "F-2026-0141", clientId: "c2", date: "20 avril 2026", amount: 60, paid: true },
    { id: "F-2026-0140", clientId: "c3", date: "18 avril 2026", amount: 38, paid: false },
    { id: "F-2026-0139", clientId: "c4", date: "17 avril 2026", amount: 72, paid: true },
    { id: "F-2026-0138", clientId: "c6", date: "15 avril 2026", amount: 45, paid: true },
  ],
  invoiceCounter: 143,
  stock: [
    { id: "st1", name: "Base coat OPI",          qty: 12, min: 5,  price: 12.90 },
    { id: "st2", name: "Top coat Gelish",        qty: 3,  min: 5,  price: 18.50 },
    { id: "st3", name: "Gel constructeur nude",  qty: 8,  min: 3,  price: 22.00 },
    { id: "st4", name: "Lingettes cleaner",      qty: 1,  min: 10, price: 4.50  },
    { id: "st5", name: "Vernis semi nude 42",    qty: 6,  min: 3,  price: 14.00 },
    { id: "st6", name: "Lime 180 grit (x10)",    qty: 24, min: 15, price: 8.00  },
  ],
  promos: [
    { id: "pr1", title: "Relance clients inactives", segment: "23 clients · sans RDV depuis 90j", status: "Programmé", detail: "Envoi samedi 10h" },
    { id: "pr2", title: "Anniversaires avril",        segment: "8 clients",                         status: "Actif",     detail: "-15% automatique" },
    { id: "pr3", title: "Créneau libre jeudi",        segment: "Clientes régulières",                status: "Brouillon", detail: "À configurer" },
  ],
  fideliteRules: { visits: 10, rewardLabel: "1 prestation offerte (jusqu'à 45 €)" },
  notifications: [
    { id: "n_seed1", type: "booking", title: "Nouvelle réservation", message: "Inès Dubois a réservé un remplissage jeudi 10h.", createdAt: Date.now() - 1000 * 60 * 47, read: false, clientId: "c3", appointmentId: "a9" },
    { id: "n_seed2", type: "payment", title: "Paiement reçu", message: "+45 € de Camille P.",                                    createdAt: Date.now() - 1000 * 60 * 60 * 3, read: false, clientId: "c2" },
    { id: "n_seed3", type: "stock",   title: "Stock bas",         message: "Top coat Gelish : il ne reste que 3 unités.",          createdAt: Date.now() - 1000 * 60 * 60 * 24, read: true },
  ],
  bookingSettings: {
    enabled: true,
    slotDuration: 30,                     // minutes between slot starts
    leadTimeMinutes: 120,                 // cannot book within X minutes of now
    preferredSocial: "instagram",         // Instagram | Facebook | TikTok | Snapchat | WhatsApp | none
    // Per-day schedule (ISO day: 1=Mon … 7=Sun) — each day optionally has a lunch break
    schedule: {
      1: { open: true,  start: 9, end: 19, break: true,  breakStart: 12.5, breakEnd: 13.5 },
      2: { open: true,  start: 9, end: 19, break: true,  breakStart: 12.5, breakEnd: 13.5 },
      3: { open: true,  start: 9, end: 19, break: true,  breakStart: 12.5, breakEnd: 13.5 },
      4: { open: true,  start: 9, end: 19, break: true,  breakStart: 12.5, breakEnd: 13.5 },
      5: { open: true,  start: 9, end: 19, break: true,  breakStart: 12.5, breakEnd: 13.5 },
      6: { open: true,  start: 9, end: 16, break: false, breakStart: 12.5, breakEnd: 13.5 },
      7: { open: false, start: 9, end: 18, break: false, breakStart: 12.5, breakEnd: 13.5 },
    },
    // Vacation / closure periods
    vacations: [],                        // [{ id, start: "2026-05-01", end: "2026-05-10", reason: "Vacances" }]
    // Legacy fields (kept for backward compat)
    hours: { start: 9, end: 18 },
    daysOpen: [1, 2, 3, 4, 5, 6],
    welcomeMessage: "",
    activeServiceIds: ["s1", "s2", "s3", "s4", "s5", "s6"],
    autoConfirm: true,
  },
};

/* ===== Public booking helper — used by the public BookingPage =====
   Pure function that returns the next data state + created entity ids.
   The caller writes it back to localStorage and re-renders. */
const commitBooking = (data, input) => {
  const { serviceId, day, h, firstName, lastName, email, phone, notes } = input;
  const fullName = `${firstName.trim()} ${lastName.trim()}`.trim();
  const service = data.services.find(s => s.id === serviceId);
  const duration = service ? service.duration : 1;

  // Match existing client by email (case-insensitive)
  const emailLower = (email || "").toLowerCase().trim();
  const existing = emailLower
    ? data.clients.find(c => (c.email || "").toLowerCase() === emailLower)
    : null;

  let clients = data.clients;
  let clientId;
  let clientWasNew = false;

  if (existing) {
    clientId = existing.id;
    clients = data.clients.map(c =>
      c.id === clientId
        ? {
            ...c,
            name: fullName || c.name,
            phone: (phone || "").trim() || c.phone,
            notes: notes && notes.trim() ? `${c.notes ? c.notes + "\n\n" : ""}${notes.trim()}` : c.notes,
          }
        : c
    );
  } else {
    clientId = "c_" + uid();
    clientWasNew = true;
    clients = [
      {
        id: clientId,
        name: fullName || "Cliente",
        email: email.trim(),
        phone: (phone || "").trim(),
        notes: (notes || "").trim(),
        visits: 0,
        spent: 0,
        fid: 0,
        last: "—",
        tags: ["Nouvelle"],
        hue: (Math.random() * 360) | 0,
      },
      ...data.clients,
    ];
  }

  const appointmentId = "a_" + uid();
  const appointment = {
    id: appointmentId,
    day, h, d: duration,
    clientId, serviceId,
    color: "accent",
    publicBooking: true,
    createdAt: Date.now(),
  };

  const notification = {
    id: "n_" + uid(),
    type: "booking",
    title: "Nouvelle réservation",
    message: `${fullName || "Un client"} a réservé ${service ? service.name : "une prestation"}.`,
    createdAt: Date.now(),
    read: false,
    clientId, appointmentId,
  };

  const nextData = {
    ...data,
    clients,
    appointments: [...data.appointments, appointment],
    notifications: [notification, ...(data.notifications || [])],
  };

  return { nextData, clientId, appointmentId, clientWasNew };
};

// Gabarit d'un "compte cloud vide" — pour qu'un nouveau compte démarre
// vraiment à zéro (aucune donnée démo : ni clients, ni RDV, ni services).
const EMPTY_CLOUD_DATA = {
  ...SEED_DATA,
  clients: [], appointments: [], invoices: [], stock: [], services: [],
  notifications: [], invoiceCounter: 1,
  messages: {}, conversations: [], activeConv: null, promos: [],
  business: { ...SEED_DATA.business, name: "", owner: "", initials: "" },
  bookingSettings: { ...SEED_DATA.bookingSettings, vacations: [] },
};

const useAppData = () => {
  const isCloud = !!(window.cbCloud && window.cbCloud.isActive());

  const [data, setData] = React.useState(() => {
    // Mode cloud : on démarre vide et on hydrate en useEffect (asynchrone)
    if (isCloud) {
      try {
        const saved = localStorage.getItem(CB_LS_KEY + "_cloud");
        if (saved) return { ...EMPTY_CLOUD_DATA, ...JSON.parse(saved) };
      } catch (e) { /* ignore */ }
      return EMPTY_CLOUD_DATA;
    }
    // Mode local (pas de session Supabase) — seed de démo comme avant
    try {
      const saved = localStorage.getItem(CB_LS_KEY);
      if (saved) return { ...SEED_DATA, ...JSON.parse(saved) };
    } catch (e) { /* ignore */ }
    return SEED_DATA;
  });

  // Charge depuis Supabase au mount + à chaque changement de session
  React.useEffect(() => {
    if (!isCloud) return;
    let cancelled = false;
    (async () => {
      try {
        const cloud = await window.cbCloud.loadAll();
        if (cancelled || !cloud) return;
        setData(prev => ({ ...prev, ...cloud }));
      } catch (e) {
        console.error("[cbCloud] loadAll failed", e);
        showToast("Impossible de charger les données (hors ligne ?)", "warn");
      }
    })();
    // Rechargement si la session change (login/logout)
    const off = window.cbSync && window.cbSync.onChange(async (online) => {
      if (!online) return;
      try {
        const cloud = await window.cbCloud.loadAll();
        if (!cancelled && cloud) setData(prev => ({ ...prev, ...cloud }));
      } catch (e) { /* ignore */ }
    });
    return () => { cancelled = true; if (off) off(); };
    // eslint-disable-next-line
  }, [isCloud]);

  // Cache localStorage par mode (cloud / local) — sert de fallback offline
  React.useEffect(() => {
    try {
      localStorage.setItem(isCloud ? CB_LS_KEY + "_cloud" : CB_LS_KEY, JSON.stringify(data));
    } catch (e) { /* quota */ }
  }, [data, isCloud]);

  // Realtime : pousse instantanément les changements DB → UI sans refresh.
  // Sert surtout aux nouvelles réservations prises depuis le lien public.
  React.useEffect(() => {
    if (!isCloud || !window.cbCloud || !window.cbCloud.subscribe) return;

    // Helper d'upsert par id immutable (insert si absent, replace si présent)
    const upsert = (arr, row) => {
      const idx = arr.findIndex(x => x.id === row.id);
      if (idx === -1) return [row, ...arr];
      const next = arr.slice();
      next[idx] = { ...next[idx], ...row };
      return next;
    };
    const dropById = (arr, id) => arr.filter(x => x.id !== id);

    const off = window.cbCloud.subscribe({
      onAppointment: ({ row, deleteId }) => setData(d => ({
        ...d,
        appointments: deleteId ? dropById(d.appointments, deleteId) : upsert(d.appointments, row),
      })),
      onClient: ({ row, deleteId }) => setData(d => ({
        ...d,
        clients: deleteId ? dropById(d.clients, deleteId) : upsert(d.clients, row),
      })),
      onInvoice: ({ row, deleteId }) => setData(d => ({
        ...d,
        invoices: deleteId ? dropById(d.invoices, deleteId) : upsert(d.invoices, row),
      })),
      onService: ({ row, deleteId }) => setData(d => ({
        ...d,
        services: deleteId ? dropById(d.services || [], deleteId) : upsert(d.services || [], row),
      })),
      onStock: ({ row, deleteId }) => setData(d => ({
        ...d,
        stock: deleteId ? dropById(d.stock, deleteId) : upsert(d.stock, row),
      })),
      onNotification: ({ row, deleteId }) => setData(d => ({
        ...d,
        notifications: deleteId
          ? dropById(d.notifications || [], deleteId)
          : upsert(d.notifications || [], row),
      })),
      onVacation: ({ row, deleteId }) => setData(d => ({
        ...d,
        bookingSettings: {
          ...(d.bookingSettings || {}),
          vacations: deleteId
            ? dropById((d.bookingSettings && d.bookingSettings.vacations) || [], deleteId)
            : upsert((d.bookingSettings && d.bookingSettings.vacations) || [], row),
        },
      })),
    });
    return off;
  }, [isCloud]);

  // Sync business info depuis le user connecté (mode local uniquement — en
  // cloud, business est chargé depuis la DB).
  React.useEffect(() => {
    if (isCloud) return;
    const user = cbAuth.getCurrentUser();
    if (!user) return;
    if (data.business && data.business.syncedFromUser === user.email) return;
    const initials = (user.ownerName || "").split(" ").map(x => x[0]).slice(0, 2).join("").toUpperCase() || "CB";
    setData(d => ({
      ...d,
      business: {
        ...d.business,
        name: user.businessName,
        owner: user.ownerName,
        initials,
        syncedFromUser: user.email,
      },
    }));
    // eslint-disable-next-line
  }, [isCloud]);

  // resetData wipes everything in the DB (cloud) ou en local. Async :
  // renvoie une promise qui résout quand c'est fait, throw en cas d'erreur
  // — pour que l'UI puisse afficher un toast d'échec.
  const resetData = async () => {
    if (isCloud) {
      await window.cbCloud.resetMyData();
      localStorage.removeItem(CB_LS_KEY + "_cloud");
      // Re-hydrate depuis le cloud (devrait être tout vide)
      const cloud = await window.cbCloud.loadAll();
      setData({ ...EMPTY_CLOUD_DATA, ...(cloud || {}) });
      return;
    }
    localStorage.removeItem(CB_LS_KEY);
    setData(EMPTY_CLOUD_DATA); // Vraiment vide, plus de SEED_DATA
  };

  return [data, setData, resetData];
};

const findClient = (data, id) => data.clients.find(c => c.id === id);
const findService = (data, id) => data.services.find(s => s.id === id);

/* ------------------- Toast system ------------------- */
let _toastListeners = new Set();
let _toastQueue = [];
const _dismissToast = (id) => {
  _toastQueue = _toastQueue.filter(x => x.id !== id);
  _toastListeners.forEach(fn => fn(_toastQueue));
};
/* showToast(msg, kind, opts?)
   opts.action = { label, onClick } → toast becomes 5s with an Undo button */
const showToast = (msg, kind = "success", opts = {}) => {
  const action = opts.action || null;
  const duration = action ? 5200 : 2800;
  const t = { id: Date.now() + Math.random(), msg, kind, action, duration };
  _toastQueue = [..._toastQueue, t];
  _toastListeners.forEach(fn => fn(_toastQueue));
  setTimeout(() => _dismissToast(t.id), duration);
};

const Toasts = () => {
  const [list, setList] = React.useState(_toastQueue);
  React.useEffect(() => {
    _toastListeners.add(setList);
    return () => { _toastListeners.delete(setList); };
  }, []);
  return (
    <div style={{
      position: "fixed", bottom: 24, right: 24, zIndex: 300,
      display: "flex", flexDirection: "column", gap: 8, pointerEvents: "none",
    }}>
      {list.map(t => (
        <div key={t.id} style={{
          padding: "12px 14px 12px 16px",
          background: "var(--surface)",
          border: "1px solid var(--line-strong)",
          borderRadius: 10,
          boxShadow: "var(--sh-2)",
          fontSize: 13.5, color: "var(--ink)",
          display: "flex", gap: 12, alignItems: "center",
          minWidth: 280, pointerEvents: "auto",
          animation: "slideIn 0.2s ease-out",
          overflow: "hidden", position: "relative",
        }}>
          <span style={{
            width: 22, height: 22, borderRadius: 50,
            background: t.kind === "success" ? "var(--sage-soft)" : t.kind === "warn" ? "oklch(96% 0.04 30)" : "var(--accent-soft)",
            color: t.kind === "success" ? "var(--sage)" : t.kind === "warn" ? "oklch(50% 0.18 30)" : "var(--accent-ink)",
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            flexShrink: 0,
          }}>
            <Icon name={t.kind === "success" ? "check" : "bell"} size={13} stroke={2.6}/>
          </span>
          <span style={{ flex: 1, minWidth: 0 }}>{t.msg}</span>
          {t.action && (
            <button
              onClick={() => { try { t.action.onClick(); } catch {} _dismissToast(t.id); }}
              style={{
                fontFamily: "inherit", fontSize: 12.5, fontWeight: 580,
                color: "var(--accent-ink)", background: "transparent",
                border: "none", padding: "4px 8px", borderRadius: 6,
                cursor: "pointer", flexShrink: 0,
              }}
              onMouseEnter={e => e.currentTarget.style.background = "var(--accent-soft)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              {t.action.label}
            </button>
          )}
          {t.action && (
            <div style={{
              position: "absolute", left: 0, bottom: 0, height: 2,
              background: "var(--accent)", opacity: 0.4,
              animation: `cbToastBar ${t.duration}ms linear forwards`,
            }}/>
          )}
        </div>
      ))}
      <style>{`@keyframes cbToastBar { from { width: 100%; } to { width: 0%; } }`}</style>
    </div>
  );
};

/* ------------------- Pulse system (discrete celebrate) ------------------- */
let _pulseListeners = new Set();
const cbPulse = (kind = "check") => {
  const p = { id: Date.now() + Math.random(), kind };
  _pulseListeners.forEach(fn => fn(p));
};
if (typeof window !== "undefined") window.cbPulse = cbPulse;

const Pulses = () => {
  const [list, setList] = React.useState([]);
  React.useEffect(() => {
    const onPulse = (p) => {
      setList(l => [...l, p]);
      setTimeout(() => setList(l => l.filter(x => x.id !== p.id)), 1100);
    };
    _pulseListeners.add(onPulse);
    return () => { _pulseListeners.delete(onPulse); };
  }, []);
  return (
    <>
      <style>{`
        @keyframes cbPulseHalo { 0% { transform: scale(.6); opacity: 0; } 35% { opacity: .55; } 100% { transform: scale(2.2); opacity: 0; } }
        @keyframes cbPulseCore { 0% { transform: scale(.4); opacity: 0; } 30% { transform: scale(1.05); opacity: 1; } 75% { transform: scale(1); opacity: 1; } 100% { transform: scale(1); opacity: 0; } }
      `}</style>
      <div style={{
        position: "fixed", inset: 0, pointerEvents: "none", zIndex: 290,
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        {list.map(p => {
          const isEuro = p.kind === "euro";
          const ring = isEuro ? "var(--sage)" : "var(--accent)";
          const bg   = isEuro ? "var(--sage-soft)" : "var(--accent-soft)";
          const ink  = isEuro ? "oklch(38% 0.10 160)" : "var(--accent-ink)";
          return (
            <div key={p.id} style={{ position: "absolute", width: 56, height: 56 }}>
              <div style={{
                position: "absolute", inset: 0, borderRadius: 50,
                border: `1.5px solid ${ring}`,
                animation: "cbPulseHalo 1000ms cubic-bezier(.22,.61,.36,1) forwards",
              }}/>
              <div style={{
                position: "absolute", inset: 0, borderRadius: 50,
                background: bg, color: ink,
                display: "flex", alignItems: "center", justifyContent: "center",
                animation: "cbPulseCore 950ms cubic-bezier(.22,.61,.36,1) forwards",
                boxShadow: "0 4px 14px -4px rgba(0,0,0,.12)",
              }}>
                <Icon name={isEuro ? "euro" : "check"} size={24} stroke={2.6}/>
              </div>
            </div>
          );
        })}
      </div>
    </>
  );
};

/* ------------------- Modal ------------------- */
const Modal = ({ open, onClose, title, children, wide, footer }) => {
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = prev;
    };
  }, [open, onClose]);

  if (!open) return null;
  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, zIndex: 200,
      background: "rgba(15, 18, 30, 0.45)",
      backdropFilter: "blur(4px)", WebkitBackdropFilter: "blur(4px)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: "var(--surface)",
        border: "1px solid var(--line)",
        borderRadius: 16, boxShadow: "var(--sh-3)",
        width: "100%", maxWidth: wide ? 640 : 440,
        maxHeight: "calc(100vh - 48px)",
        display: "flex", flexDirection: "column",
        overflow: "hidden",
      }}>
        <div style={{
          padding: "18px 22px",
          borderBottom: "1px solid var(--line)",
          display: "flex", justifyContent: "space-between", alignItems: "center",
          flexShrink: 0,
        }}>
          <div style={{
            fontFamily: "var(--ff-display)", fontSize: 17,
            fontWeight: 580, letterSpacing: "-0.02em",
          }}>{title}</div>
          <button onClick={onClose} aria-label="Fermer" style={{
            background: "transparent", border: "none", cursor: "pointer",
            color: "var(--ink-4)", padding: 4, display: "flex",
          }}>
            <Icon name="close" size={18}/>
          </button>
        </div>
        <div style={{ padding: 22, overflow: "auto", flex: 1 }}>{children}</div>
        {footer && (
          <div style={{
            padding: "16px 22px 18px", borderTop: "1px solid var(--line)",
            background: "var(--surface)",
            display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 10,
            flexShrink: 0,
          }}>{footer}</div>
        )}
      </div>
    </div>
  );
};

/* ------------------- Form primitives ------------------- */
const fieldLabelStyle = {
  display: "block", fontSize: 12.5, fontWeight: 500,
  color: "var(--ink-2)", marginBottom: 6,
};
const fieldInputStyle = {
  width: "100%", maxWidth: "100%", minWidth: 0,
  display: "block",
  padding: "10px 12px",
  fontSize: 16, fontFamily: "inherit",
  background: "var(--bg)",
  border: "1px solid var(--line-strong)",
  borderRadius: 9, color: "var(--ink)", outline: "none",
  boxSizing: "border-box",
  // iOS Safari : neutralise la largeur intrinsèque des input date/time
  WebkitAppearance: "none",
  appearance: "none",
};

const FormField = ({ label, type = "text", value, onChange, placeholder, required, autoFocus, disabled }) => (
  <div>
    <label style={fieldLabelStyle}>{label}{required && <span style={{ color: "var(--accent)" }}> *</span>}</label>
    <input type={type} value={value} onChange={e => onChange(e.target.value)}
      placeholder={placeholder} required={required} autoFocus={autoFocus} disabled={disabled}
      style={{ ...fieldInputStyle, opacity: disabled ? 0.55 : 1 }}/>
  </div>
);

const FormSelect = ({ label, value, onChange, options, required }) => (
  <div>
    <label style={fieldLabelStyle}>{label}{required && <span style={{ color: "var(--accent)" }}> *</span>}</label>
    <select value={value} onChange={e => onChange(e.target.value)} required={required}
      style={{ ...fieldInputStyle, appearance: "none", backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238889a0' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E\")", backgroundRepeat: "no-repeat", backgroundPosition: "right 10px center", backgroundSize: 16, paddingRight: 34 }}>
      {options.map(o => {
        const [val, lbl] = Array.isArray(o) ? o : [o, o];
        return <option key={val} value={val}>{lbl}</option>;
      })}
    </select>
  </div>
);

const FormTextarea = ({ label, value, onChange, placeholder, rows = 3 }) => (
  <div>
    <label style={fieldLabelStyle}>{label}</label>
    <textarea value={value} onChange={e => onChange(e.target.value)}
      placeholder={placeholder} rows={rows}
      style={{ ...fieldInputStyle, resize: "vertical", minHeight: rows * 24, fontFamily: "inherit" }}/>
  </div>
);

/* Global style for toast animation */
(function injectCbKeyframes() {
  if (document.getElementById("cb-keyframes")) return;
  const style = document.createElement("style");
  style.id = "cb-keyframes";
  style.textContent = `
    @keyframes slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
    .cb-row-hover:hover { background: var(--bg-alt) !important; cursor: pointer; }
    .cb-hide-scrollbar { scrollbar-width: none; -ms-overflow-style: none; }
    .cb-hide-scrollbar::-webkit-scrollbar { display: none; width: 0; height: 0; }
  `;
  document.head.appendChild(style);
})();

Object.assign(window, {
  useAppData, findClient, findService, uid, slugify,
  Modal, Toasts, showToast,
  FormField, FormSelect, FormTextarea,
  fieldLabelStyle, fieldInputStyle,
  cbAuth, useCurrentUser, CookieBanner,
  commitBooking, CB_LS_KEY, SEED_DATA,
});
