// Pau Fit — root app: navigation, theme, tweaks integration

const { useState: useSt, useEffect: useEf, useMemo } = React;

// ─────────────────────────────────────────────────────────────
// PauFriend — mensajes cercanos sin piropos, estilo amiga real.
// Se usa en notificaciones y banners de motivación. NO usar "linda",
// "reina", "bonita", "preciosa" — solo cariño normal y directo.
// ─────────────────────────────────────────────────────────────
window.PauFriend = window.PauFriend || (function() {
  // Mensajes para distintas situaciones, sin diminutivos exagerados
  const POOLS = {
    daily: [
      'Hoy también cuenta. Vamos',
      'Ya estás aquí, eso es lo difícil',
      '¿Pasamos por tu rutina?',
      'Hoy 20 min y listo',
      'Te toca día {day}, todo listo',
      'Pequeño ratito y a otra cosa',
      'No tiene que ser perfecto, solo hoy',
      'Empezar es la parte más dura, y ya casi',
      'Vamos, que mañana lo agradeces',
    ],
    streakKeep: [
      'Llevas {streak} días seguidos, sigue',
      '{streak} días — no rompas eso hoy',
      'Tu racha de {streak} te está esperando',
    ],
    streakBroken: [
      'Hace unos días que no pasas, vamos hoy',
      'Volver es lo importante, no cuándo',
      'Reinicia hoy sin culpa',
    ],
    mealReminder: [
      'No olvides anotar lo que comiste',
      '¿Qué tal la comida de hoy?',
      'Cuenta lo del almuerzo cuando puedas',
    ],
    planReady: [
      'Tu plan está listo, échale un vistazo',
      'Listo — abre la app cuando puedas',
      'Tu plan personalizado te espera',
    ],
    sweetCravingTitle: 'Antojo dulce',
    // Cada antojo es una RECETA completa: con nombre, emoji, ingredientes y pasos.
    // La foto se genera vía /api/recipes/image como cualquier receta del plan.
    // Al tocar uno se abre MealRecipeModal con todos los detalles.
    sweetCravingIdeas: [
      {
        name: 'Yogur griego con banana y canela', emoji: '🍌',
        kcal: 220, protein_g: 14, carbs_g: 32, fat_g: 4, time: '2 min',
        mealType: 'Snack',
        ingredients: ['150g yogur griego natural', '1 banana madura (≈120g)', 'Canela al gusto'],
        instructions: 'Sirve el yogur en un bowl. Corta la banana en rodajas y ponla encima. Espolvorea canela. Listo.',
      },
      {
        name: 'Yogur griego con pasas sultanas', emoji: '🍇',
        kcal: 210, protein_g: 14, carbs_g: 28, fat_g: 4, time: '1 min',
        mealType: 'Snack',
        ingredients: ['150g yogur griego natural', '1 cda (15g) pasas sultanas', 'Pizca de canela'],
        instructions: 'Pon el yogur en un bowl, añade las pasas por encima y un toque de canela. Hidrátalas 5 min antes si las quieres más jugosas.',
      },
      {
        name: 'Yogur con crema de maní', emoji: '🥜',
        kcal: 280, protein_g: 16, carbs_g: 14, fat_g: 18, time: '1 min',
        mealType: 'Snack',
        ingredients: ['150g yogur griego natural', '1 cdta (10g) crema de maní natural', 'Pizca de canela'],
        instructions: 'Sirve el yogur, añade la crema de maní en el centro y mezcla un poco para que se derrita.',
      },
      {
        name: 'Dátiles rellenos de crema de maní', emoji: '🌰',
        kcal: 180, protein_g: 4, carbs_g: 28, fat_g: 7, time: '3 min',
        mealType: 'Snack',
        ingredients: ['2 dátiles medjool sin hueso', '1 cdta (10g) crema de maní natural', 'Pizca de sal (opcional)'],
        instructions: 'Abre cada dátil y rellena con la crema de maní. Espolvorea sal si quieres. Cómelos fríos o tibios.',
      },
      {
        name: 'Banana congelada bañada en chocolate', emoji: '🍌',
        kcal: 230, protein_g: 3, carbs_g: 40, fat_g: 8, time: '5 min + frío',
        mealType: 'Snack',
        ingredients: ['1 banana (≈120g)', '20g chocolate negro 85%', 'Coco rallado al gusto (opcional)'],
        instructions: 'Corta la banana en rodajas y congélala 1h. Derrite el chocolate al baño maría. Baña cada rodaja y espolvorea coco. Vuelve al congelador 10 min.',
      },
      {
        name: 'Yogur griego con miel y canela', emoji: '🍯',
        kcal: 200, protein_g: 14, carbs_g: 22, fat_g: 4, time: '1 min',
        mealType: 'Snack',
        ingredients: ['150g yogur griego natural', '1 cdta (7g) miel cruda', 'Canela en polvo'],
        instructions: 'Sirve el yogur en un bowl. Vierte la miel en hilo por encima y espolvorea canela.',
      },
      {
        name: 'Fresas con chocolate negro', emoji: '🍓',
        kcal: 160, protein_g: 2, carbs_g: 20, fat_g: 8, time: '5 min',
        mealType: 'Snack',
        ingredients: ['150g fresas frescas', '15g chocolate negro 85%'],
        instructions: 'Lava y seca las fresas. Derrite el chocolate al baño maría. Moja cada fresa por la base y déjalas enfriar sobre papel de hornear.',
      },
      {
        name: 'Manzana asada con canela', emoji: '🍎',
        kcal: 130, protein_g: 1, carbs_g: 32, fat_g: 0, time: '20 min',
        mealType: 'Snack',
        ingredients: ['1 manzana (≈180g)', '1 cdta canela', 'Pizca de nuez moscada (opcional)'],
        instructions: 'Precalienta el horno a 180°C. Corta la manzana en cuartos sin pelar. Espolvorea canela y nuez moscada. Hornea 18-20 min hasta que esté tierna.',
      },
      {
        name: 'Smoothie de frutos rojos', emoji: '🫐',
        kcal: 200, protein_g: 12, carbs_g: 28, fat_g: 4, time: '3 min',
        mealType: 'Snack',
        ingredients: ['150g frutos rojos congelados', '150g yogur griego', '50ml bebida vegetal sin azúcar'],
        instructions: 'Pon todo en la batidora y tritura hasta cremoso. Si quieres más espeso, menos bebida vegetal.',
      },
      {
        name: 'Tortita de arroz con banana y canela', emoji: '🍞',
        kcal: 180, protein_g: 3, carbs_g: 38, fat_g: 1, time: '2 min',
        mealType: 'Snack',
        ingredients: ['1 tortita de arroz integral', '1 banana pequeña (≈100g)', 'Canela al gusto'],
        instructions: 'Corta la banana en rodajas finas y colócalas sobre la tortita. Espolvorea canela.',
      },
      {
        name: 'Bombones fit de dátil', emoji: '🍫',
        kcal: 180, protein_g: 3, carbs_g: 22, fat_g: 9, time: '15 min',
        mealType: 'Snack',
        ingredients: ['2 dátiles medjool sin hueso', '1 cdta crema de maní', '15g chocolate negro 85%'],
        instructions: 'Rellena los dátiles con la crema de maní. Derrite el chocolate y baña cada dátil. Refrigera 10 min hasta que el chocolate se endurezca.',
      },
    ],
  };
  function pick(pool, params = {}) {
    if (!pool || pool.length === 0) return '';
    let msg = pool[Math.floor(Math.random() * pool.length)];
    Object.entries(params).forEach(([k, v]) => {
      msg = msg.replace(new RegExp('\\{' + k + '\\}', 'g'), v);
    });
    return msg;
  }
  function daily(params) { return pick(POOLS.daily, params); }
  function streak(streakDays) {
    if (streakDays >= 1) return pick(POOLS.streakKeep, { streak: streakDays });
    return pick(POOLS.streakBroken);
  }
  function mealReminder() { return pick(POOLS.mealReminder); }
  function planReady() { return pick(POOLS.planReady); }
  function sweetIdeas() { return POOLS.sweetCravingIdeas; }
  return { daily, streak, mealReminder, planReady, sweetIdeas };
})();

// ─────────────────────────────────────────────────────────────
// PauIngredientTweaks — ajustes de ingredientes por receta
// Persiste en localStorage: { [recipeKey]: { [idx]: { have, override } } }
//   have      → "ya lo tengo" — se omite de la lista de la compra
//   override  → texto sustituto del ingrediente (cambiar cantidad o nombre)
// ─────────────────────────────────────────────────────────────
window.PauIngredientTweaks = window.PauIngredientTweaks || (function() {
  const STORAGE_KEY = 'paufit-ing-tweaks';
  function recipeKey(name) {
    return (name || '').toLowerCase()
      .normalize('NFD').replace(/[̀-ͯ]/g, '')
      .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
  }
  function readAll() {
    try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') || {}; }
    catch { return {}; }
  }
  function writeAll(all) {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(all)); }
    catch (e) { console.warn('[ing-tweaks] write failed:', e?.message); }
    try { window.dispatchEvent(new CustomEvent('pau:ing-tweaks-updated')); } catch {}
  }
  function read(recipeName) {
    return readAll()[recipeKey(recipeName)] || {};
  }
  function update(recipeName, idx, patch) {
    const all = readAll();
    const k = recipeKey(recipeName);
    all[k] = all[k] || {};
    const cur = all[k][idx] || {};
    const next = { ...cur, ...patch };
    // Limpieza: si queda vacío de info, borramos la entrada
    if (!next.have && !next.override) delete all[k][idx];
    else all[k][idx] = next;
    if (Object.keys(all[k]).length === 0) delete all[k];
    writeAll(all);
  }
  function clearOne(recipeName, idx) {
    const all = readAll();
    const k = recipeKey(recipeName);
    if (all[k]) {
      delete all[k][idx];
      if (Object.keys(all[k]).length === 0) delete all[k];
      writeAll(all);
    }
  }
  // ─── Recalculo de macros cuando se cambia cantidad ───
  // Si el ingrediente original es "200g pollo" y el override es "100g pollo",
  // detectamos el ratio (0.5) y devolvemos macros escaladas para esa receta.
  // Si no se puede parsear, ratio = 1 (sin cambio).
  function extractQty(s) {
    const m = String(s || '').match(/^(\d+(?:[.,]\d+)?)\s*(kg|g|gr|gramos|ml|l|cda|cdta|u|unidades?)?/i);
    if (!m) return null;
    let qty = parseFloat(m[1].replace(',', '.'));
    let unit = (m[2] || 'u').toLowerCase();
    if (unit === 'kg') { qty *= 1000; unit = 'g'; }
    if (unit === 'l') { qty *= 1000; unit = 'ml'; }
    if (unit === 'gr' || unit === 'gramos') unit = 'g';
    return { qty, unit };
  }
  // Devuelve la receta con kcal/macros recalculados según los tweaks (overrides de qty)
  // Si una persona dice "200g pollo → 100g pollo", reducimos kcal y macros a la mitad.
  // Si hay varios overrides, los aplicamos en cadena (ratio combinado).
  function applyRecipeAdjustments(meal) {
    if (!meal || !Array.isArray(meal.ingredients)) return meal;
    const tweaks = read(meal.name);
    if (!tweaks || Object.keys(tweaks).length === 0) return meal;
    let totalRatio = 1; let count = 0;
    meal.ingredients.forEach((ing, idx) => {
      const tw = tweaks[idx];
      if (!tw) return;
      if (tw.have) {
        // "Ya lo tengo" no cambia macros (la comida es la misma) — solo afecta la compra
        return;
      }
      if (tw.override) {
        const origQty = extractQty(ing);
        const newQty = extractQty(tw.override);
        if (origQty && newQty && origQty.qty > 0 && origQty.unit === newQty.unit) {
          const r = newQty.qty / origQty.qty;
          totalRatio += r; count++;
        }
      }
    });
    if (count === 0) return meal;
    // Promedio de ratios — porque la receta tiene varios ingredientes
    // y solo unos cuantos cambian de cantidad. Un ratio promedio refleja
    // mejor el cambio agregado que multiplicar uno solo.
    const avgRatio = (totalRatio - 1) / count;
    if (Math.abs(avgRatio - 1) < 0.05) return meal;
    return {
      ...meal,
      kcal: meal.kcal ? Math.round(meal.kcal * avgRatio) : meal.kcal,
      protein_g: meal.protein_g ? Math.round(meal.protein_g * avgRatio) : meal.protein_g,
      carbs_g: meal.carbs_g ? Math.round(meal.carbs_g * avgRatio) : meal.carbs_g,
      fat_g: meal.fat_g ? Math.round(meal.fat_g * avgRatio) : meal.fat_g,
      _adjusted: true,
    };
  }
  return { recipeKey, read, update, clear: clearOne, applyRecipeAdjustments };
})();

// ─────────────────────────────────────────────────────────────
// PauSubstitutions — sugerencias de sustitución cuando no tienes algo
// Mapeo Pau Fit: para cada ingrediente común, 3 alternativas con el mismo perfil.
// Se usa en MealRecipeModal cuando tocas "🤷 NO TENGO".
// ─────────────────────────────────────────────────────────────
window.PauSubstitutions = window.PauSubstitutions || (function() {
  // Familias de ingredientes — cada uno → 3 sustitutos comunes Pau Fit
  const TABLE = {
    // Proteínas
    'pollo':       ['pavo', 'pescado blanco', 'tofu firme'],
    'pavo':        ['pollo', 'atún en agua', 'tofu firme'],
    'atun':        ['salmón', 'pollo', 'huevos'],
    'salmon':      ['atún en agua', 'pescado blanco', 'sardinas'],
    'sardinas':    ['atún en agua', 'salmón'],
    'huevos':      ['claras de huevo', 'tofu revuelto'],
    'claras':      ['huevos enteros', 'tofu revuelto'],
    'tofu':        ['tempeh', 'pollo', 'huevos'],
    'jamon':       ['pavo', 'pollo cocido', 'atún en agua'],
    'queso':       ['queso fresco', 'requesón', 'cottage'],
    'yogur':       ['yogur natural', 'kéfir', 'requesón'],
    // Carbohidratos
    'arroz':       ['patata', 'batata', 'pasta integral'],
    'patata':      ['batata', 'arroz', 'pan integral'],
    'batata':      ['patata', 'arroz', 'avena'],
    'pasta':       ['arroz', 'patata', 'fideos integrales'],
    'avena':       ['copos de espelta', 'pan integral', 'tortita arroz'],
    'pan':         ['tortita arroz', 'tortita maíz', 'avena'],
    'tortita':     ['pan integral', 'avena'],
    // Grasas
    'aguacate':    ['humus', 'crema de maní', 'queso fresco'],
    'aove':        ['aceite coco', 'aguacate', 'crema maní'],
    'crema':       ['aguacate', 'tahini', 'humus'],
    'almendras':   ['nueces', 'anacardos', 'pistachos'],
    'nueces':      ['almendras', 'anacardos', 'avellanas'],
    // Fruta
    'banana':      ['manzana', 'pera', 'mango'],
    'platano':     ['manzana', 'pera', 'mango'],
    'manzana':     ['pera', 'banana', 'durazno'],
    'fresas':      ['frutos rojos', 'manzana', 'mango'],
    'arandanos':   ['fresas', 'frambuesas', 'mora'],
    // Verduras
    'tomate':      ['pimiento', 'pepino', 'zanahoria'],
    'brocoli':     ['coliflor', 'espárragos', 'judías verdes'],
    'espinaca':    ['rúcula', 'kale', 'canónigos'],
    'pepino':      ['apio', 'calabacín'],
    'zanahoria':   ['apio', 'pepino'],
    // Dulce
    'datiles':     ['miel', 'pasas', 'higos'],
    'miel':        ['dátiles', 'sirope arce'],
    'chocolate':   ['cacao puro', 'algarroba'],
  };
  // Detector simple: busca la palabra clave en el texto del ingrediente
  function suggest(ingredientText) {
    const lower = String(ingredientText || '')
      .toLowerCase()
      .normalize('NFD').replace(/[̀-ͯ]/g, '');
    for (const key of Object.keys(TABLE)) {
      if (lower.includes(key)) return { key, options: TABLE[key] };
    }
    // Genérico si no detectamos: 3 alternativas neutras saludables
    return { key: 'general', options: ['Ingrediente similar', 'Omítelo si no es clave', 'Pregunta en la comunidad'] };
  }
  return { suggest };
})();

// ─────────────────────────────────────────────────────────────
// Palettes — SWEAT-inspired: punch, contrast, energy
// ─────────────────────────────────────────────────────────────
const PALETTES = {
  cream: {
    // Look Apple premium (2026-07-09, pedido de Pau): lienzo neutro casi
    // blanco y el rosa SOLO como acento. Si todo es rosa, nada es rosa.
    name: 'Cream',
    accent: '#E15577', deep: '#B23A56',
    bg: '#FAF8F7', card: '#FFFFFF',
    text: '#1C1B1D', muted: '#8E888B', subtle: '#F1EDEB',
    soft: '#F7ECEA', warm: '#F0DEDA',
    gold: '#C4A57E',
    border: '0.5px solid rgba(28,27,29,0.07)', dark: false,
  },
  sand: {
    name: 'Sand',
    accent: '#B89181', deep: '#7E5B4F',
    bg: '#F0E6D8', card: '#FAF4EB',
    text: '#3A2C24', muted: '#A89789', subtle: '#E5D6C2',
    soft: '#EBDDC9', warm: '#DECCB4',
    gold: '#C2A47C',
    border: '0.5px solid rgba(58,44,36,0.06)', dark: false,
  },
  rose: {
    name: 'Dusty Rose',
    accent: '#C58A8A', deep: '#8A5757',
    bg: '#F3EAE6', card: '#FAF3EE',
    text: '#3B2A2A', muted: '#AC9590', subtle: '#E8D5CF',
    soft: '#EDDDD6', warm: '#E0C7BE',
    gold: '#C5A47B',
    border: '0.5px solid rgba(59,42,42,0.06)', dark: false,
  },
  sage: {
    name: 'Sage',
    accent: '#9DAE9A', deep: '#5D6F5A',
    bg: '#ECE9DF', card: '#F7F4EC',
    text: '#33372F', muted: '#9DA295', subtle: '#DAD7CA',
    soft: '#E0DDD0', warm: '#D1CFC0',
    gold: '#BFA77A',
    border: '0.5px solid rgba(51,55,47,0.06)', dark: false,
  },
  linen: {
    name: 'Linen',
    accent: '#B5A28F', deep: '#7C6A58',
    bg: '#F2EDE5', card: '#FBF7EF',
    text: '#3A332A', muted: '#A89A89', subtle: '#E6DCCC',
    soft: '#ECE2D2', warm: '#DDD0BC',
    gold: '#BFA47A',
    border: '0.5px solid rgba(58,51,42,0.06)', dark: false,
  },
  dark: {
    name: 'Noche',
    accent: '#E15577', deep: '#B23A56',
    bg: '#181318', card: '#221C22',
    text: '#F4ECEE', muted: '#857880', subtle: '#2C2428',
    soft: '#27221E', warm: '#352E2A',
    gold: '#C4A57E',
    border: '0.5px solid rgba(255,255,255,0.05)', dark: true,
  },
};

const PALETTE_META = [
  { id: 'cream', hint: 'Rosa Pau Fit',                free: true,  swatches: ['#E15577','#FBE0E3','#FCEBEC'] },
  { id: 'sand',  hint: 'Sand cálido',                free: false, swatches: ['#B89181','#E5D6C2','#F0E6D8'] },
  { id: 'rose',  hint: 'Dusty rose tranquilo',       free: false, swatches: ['#C58A8A','#E8D5CF','#F3EAE6'] },
  { id: 'sage',  hint: 'Sage verde calma',           free: false, swatches: ['#9DAE9A','#DAD7CA','#ECE9DF'] },
  { id: 'linen', hint: 'Linen neutro',               free: false, swatches: ['#B5A28F','#E6DCCC','#F2EDE5'] },
];
window.PALETTE_META = PALETTE_META;

// ─────────────────────────────────────────────────────────────
// Typography presets
// ─────────────────────────────────────────────────────────────
const FONT_PRESETS = {
  poppins: {
    name: 'Poppins',
    hint: 'La fuente original de Pau Fit (predeterminada)',
    body: "'Poppins', system-ui, -apple-system, sans-serif",
    display: "'Poppins', system-ui, -apple-system, sans-serif",
  },
  geomanist: {
    name: 'Geomanist',
    hint: 'La fuente REAL de Silbe (extraída de su APK)',
    body: "'Geomanist', system-ui, -apple-system, sans-serif",
    display: "'Geomanist', system-ui, -apple-system, sans-serif",
  },
  newsreader: {
    name: 'Newsreader',
    hint: 'Editorial moderno — similar a New York',
    body: "'Inter', system-ui, -apple-system, sans-serif",
    display: "'Newsreader', 'New York', Georgia, serif",
  },
  dmserif: {
    name: 'DM Serif',
    hint: 'Alto contraste · fashion',
    body: "'Inter', system-ui, -apple-system, sans-serif",
    display: "'DM Serif Display', Georgia, serif",
  },
  playfair: {
    name: 'Playfair',
    hint: 'Clásico elegante',
    body: "'Inter', system-ui, -apple-system, sans-serif",
    display: "'Playfair Display', Georgia, serif",
  },
  sourceserif: {
    name: 'Source Serif',
    hint: 'Minimal premium',
    body: "'Inter', system-ui, sans-serif",
    display: "'Source Serif 4', Georgia, serif",
  },
  fraunces: {
    name: 'Fraunces',
    hint: 'Curvado · romántico',
    body: "'Inter', system-ui, sans-serif",
    display: "'Fraunces', Georgia, serif",
  },
  newyork: {
    name: 'NY · Poppins',
    hint: 'Como en paufit.co',
    body: "'Poppins', system-ui, sans-serif",
    display: "ui-serif, 'New York', 'Newsreader', Georgia, serif",
  },
  inter: {
    name: 'Inter clean',
    hint: 'Sans moderno',
    body: "'Inter', system-ui, sans-serif",
    display: "'Inter', system-ui, sans-serif",
  },
};
window.FONT_PRESETS = FONT_PRESETS;

// ─────────────────────────────────────────────────────────────
// App root
// ─────────────────────────────────────────────────────────────
function App() {
  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "palette": "cream",
    "font": "poppins",
    "isPremium": true,
    "darkMode": false,
    "isPau": false,
    "joinedRetos": []
  }/*EDITMODE-END*/;

  // Emails con acceso premium automático (sin necesidad de pagar Stripe).
  // Pau y admins siempre tienen modo god. El resto respeta profile.is_premium.
  const GODMODE_EMAILS = ['paulasmareyes@gmail.com'];

  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [tab, setTab] = useSt('home');
  const [stack, setStack] = useSt([]);
  const [transition, setTransition] = useSt(null);
  const [modal, setModalEl] = useSt(null);
  const [toast, setToast] = useSt(null);
  // CTA sticky inferior (estilo Sweat/BODi) — vive FUERA del scroll-area
  // para que sea inmune a transforms ancestros. Las pantallas que necesiten
  // un botón sticky lo registran con nav.showCTA / nav.hideCTA.
  const [bottomCTA, setBottomCTA] = useSt(null);

  // ── Auth gate via Supabase ─────────────────────────────────
  const [session, setSession] = useSt(null);
  const [authChecked, setAuthChecked] = useSt(false);
  const [dataReady, setDataReady] = useSt(false);

  useEf(() => {
    const sb = window.__PAU_SUPABASE;
    if (!sb) { setAuthChecked(true); setDataReady(true); return; }
    sb.auth.getSession().then(({ data }) => {
      setSession(data.session);
      setAuthChecked(true);
    });
    const { data: { subscription } } = sb.auth.onAuthStateChange((_event, s) => {
      setSession(s);
    });
    return () => subscription && subscription.unsubscribe();
  }, []);

  // ── Precarga de datos antes de mostrar app ────────────────
  useEf(() => {
    if (!authChecked) return;
    if (!session) { setDataReady(true); return; }
    const sb = window.__PAU_SUPABASE;
    if (!sb) { setDataReady(true); return; }
    let abort = false;
    (async () => {
      window.__pauCache = window.__pauCache || {};
      const profilePromise = sb.from('profiles')
        .select('id, full_name, username, avatar_url, bio, is_premium, premium_until, trial_ends_at, points, level, goal, target_days_per_week, height_cm, weight_kg, birth_date, gender, phone, is_public, username_changes, created_at')
        .eq('id', session.user.id).maybeSingle()
        .then(({ data }) => {
          const enriched = { ...(data || {}), email: session.user.email || '' };
          window.__pauCache.profile = enriched;
          return enriched;
        })
        .catch(() => ({ email: session.user.email || '' }));
      const activeRetoPromise = window.PauUserRetos
        ? window.PauUserRetos.getActive().then(async (d) => {
            // AUTO-REPARACIÓN: si el array completed_days tiene días de una
            // pasada anterior (timestamps previos a enrolled_at), recalcular
            // desde day_completions reales y corregir la fila una sola vez.
            try {
              if (d && d.enrolled_at) {
                const { data: dcs } = await sb.from('day_completions')
                  .select('day_number, completed_at')
                  .eq('user_id', session.user.id)
                  .eq('reto_id', d.reto_id)
                  .gte('completed_at', d.enrolled_at);
                const reales = [...new Set((dcs || []).map(x => x.day_number))].sort((a, b) => a - b);
                const actuales = (d.completed_days || []).slice().sort((a, b) => a - b);
                if (JSON.stringify(reales) !== JSON.stringify(actuales)) {
                  const nextDay = reales.length ? Math.min(Math.max(...reales) + 1, 999) : 1;
                  await sb.from('user_retos')
                    .update({ completed_days: reales, current_day: nextDay })
                    .eq('id', d.id);
                  d = { ...d, completed_days: reales, current_day: nextDay };
                }
              }
            } catch (e) { console.warn('[self-heal retos]', e?.message); }
            window.__pauCache.activeReto = d;
            return d;
          }).catch(() => null)
        : Promise.resolve(null);
      const statsPromise = window.PauUserRetos
        ? window.PauUserRetos.getStats().then(s => {
            window.__pauCache.stats = s;
            if (s && window.PauUserRetos.computeAchievements) {
              window.__pauCache.achievements = window.PauUserRetos.computeAchievements(s);
            }
            return s;
          }).catch(() => null)
        : Promise.resolve(null);
      const rutinasPromise = sb.from('rutinas').select('*').eq('is_published', true)
        .order('is_demo', { ascending: false }).order('position', { ascending: true })
        .then(({ data }) => { window.__pauCache.rutinas = data || []; return data; })
        .catch(() => null);
      // Retos REALES desde la BD (adaptados al formato del catálogo).
      // Mientras Pau no publique retos propios, pauRetos() cae al mock.
      const retosPromise = sb.from('retos').select('*').eq('is_published', true)
        .order('position', { ascending: true })
        .then(({ data }) => {
          window.__pauCache.retos = (data || []).map(r => {
            // Sin portada subida aún → usar la foto del catálogo local si el
            // id coincide (los retos sembrados comparten ids con el catálogo)
            const local = (typeof RETOS !== 'undefined') ? RETOS.find(m => m.id === r.id) : null;
            return {
              id: r.id, emoji: r.emoji || '🔥', title: r.title, subtitle: r.subtitle,
              desc: r.description || r.subtitle || '', days: r.days || 14,
              minPerDay: r.minutes_per_day || 20, intensity: r.intensity || 'Medio',
              category: r.category || 'Cuerpo completo',
              tag: r.is_premium ? 'PRO' : 'GRATIS',
              color: r.color || (local && local.color) || '#E15577',
              bg: r.bg_color || (local && local.bg) || '#FBE0E3',
              image: r.image_url || (local && local.image) || null,
              coach: r.coach || 'Pau Moreno',
            };
          });
          return data;
        })
        .catch(() => null);
      // Programas por semanas: catálogo + programa activo + sus días
      const programasPromise = window.PauUserProgramas
        ? (async () => {
            const catalog = await window.PauUserProgramas.getCatalog();
            window.__pauCache.programas = catalog;
            const activeProg = await window.PauUserProgramas.getActive();
            window.__pauCache.activePrograma = activeProg;
            if (activeProg) {
              window.__pauCache.activeProgramaDays = await window.PauUserProgramas.getDays(activeProg.programa_id);
            }
            return catalog;
          })().catch(() => null)
        : Promise.resolve(null);
      const timeout = new Promise(r => setTimeout(r, 4000));
      await Promise.race([Promise.all([profilePromise, activeRetoPromise, statsPromise, rutinasPromise, programasPromise, retosPromise]), timeout]);
      if (abort) return;
      const cachedProfile = window.__pauCache.profile;
      // PREMIUM = TODOS. Modelo "fuera de la app" estilo Spotify:
      // si tienes la app, ya estás suscrita (la compra se hace antes en la web).
      // Cuando llegue el momento de paywall real (App Store), se cambia esto.
      setTweak('isPremium', true);
      // Godmode flag (Pau y admins) — para UI especial en algunos sitios
      if (cachedProfile) {
        const email = (cachedProfile.email || session.user?.email || '').toLowerCase();
        if (GODMODE_EMAILS.includes(email)) {
          window.__pauCache.isGodmode = true;
        }
      }

      // SYNC localState con Supabase: si hay reto activo en cache, asegurar
      // que tweaks.joinedRetos lo incluye. Resuelve el bug de "Continuar reto"
      // pidiendo unirse otra vez tras limpiar localStorage o cambiar de device.
      const activeReto = window.__pauCache.activeReto;
      if (activeReto && activeReto.reto_id) {
        const current = (tweaks.joinedRetos || []);
        if (!current.includes(activeReto.reto_id)) {
          setTweak('joinedRetos', [...current, activeReto.reto_id]);
        }
      }

      // AUTO-DISPARAR ONBOARDING — para usuarias nuevas sin objetivo configurado
      // (después de signup, la primera vez que entran a la app).
      const profile = window.__pauCache.profile;
      if (profile && !profile.goal && !profile.target_days_per_week && !activeReto) {
        // Aún no completó el cuestionario y no tiene reto activo → mostrar onboarding
        setTimeout(() => setStack([{ kind: 'onboarding', id: 'auto' }]), 200);
      }

      // TUTORIAL inicial — solo la primera vez (localStorage flag)
      try {
        if (!localStorage.getItem('paufit-tutorial-seen')) {
          setTimeout(() => {
            setModalEl(<WelcomeTutorialModal T={(tweaks.darkMode ? PALETTES.dark : PALETTES[tweaks.palette] || PALETTES.cream)}
              onDone={() => {
                try { localStorage.setItem('paufit-tutorial-seen', '1'); } catch {}
                setModalEl(null);
              }}/>);
          }, 600);
        }
      } catch {}

      setDataReady(true);
    })();
    function onFocus() {
      // Premium siempre true mientras el modelo sea "fuera de la app"
      setTweak('isPremium', true);
    }
    window.addEventListener('focus', onFocus);
    return () => { abort = true; window.removeEventListener('focus', onFocus); };
  }, [authChecked, session?.user?.id]);

  // Joined retos — Set of reto IDs the user has joined
  const joinedRetos = useMemo(() => new Set(tweaks.joinedRetos || []), [tweaks.joinedRetos]);
  const joinReto = (id) => {
    if (!joinedRetos.has(id)) setTweak('joinedRetos', [...(tweaks.joinedRetos || []), id]);
  };
  const leaveReto = async (id) => {
    // State local
    setTweak('joinedRetos', (tweaks.joinedRetos || []).filter(x => x !== id));
    // Supabase REAL: marcar el user_retos como completed_at (= abandonado)
    try {
      const sb = window.__PAU_SUPABASE;
      const cache = window.__pauCache || {};
      const userReto = cache.activeReto;
      if (sb && userReto && userReto.id && userReto.reto_id === id && window.PauUserRetos) {
        await window.PauUserRetos.abandon(userReto.id);
        // limpiar cache
        window.__pauCache.activeReto = null;
        window.dispatchEvent(new CustomEvent('pau:cache-updated'));
      }
    } catch (e) {
      console.warn('[leaveReto] error:', e?.message || e);
    }
  };

  const theme = useMemo(() => {
    if (tweaks.darkMode) return PALETTES.dark;
    return PALETTES[tweaks.palette] || PALETTES.cream;
  }, [tweaks.palette, tweaks.darkMode]);

  useEf(() => {
    const preset = FONT_PRESETS[tweaks.font] || FONT_PRESETS.poppins;
    const root = document.documentElement;
    root.style.setProperty('--font-body', preset.body);
    root.style.setProperty('--font-display', preset.display);
  }, [tweaks.font]);

  const nav = useMemo(() => ({
    go: (kind, id) => {
      setTransition('push');
      setStack(s => [...s, { kind, id }]);
      setTimeout(() => setTransition(null), 340);
    },
    back: () => {
      setTransition('pop');
      setTimeout(() => {
        setStack(s => s.slice(0, -1));
        setTransition(null);
      }, 280);
    },
    tab: (t) => {
      setStack([]);
      setTab(t);
    },
    openModal: (el) => setModalEl(el),
    closeModal: () => setModalEl(null),
    toast: (msg, opts = {}) => {
      setToast({ msg, ...opts });
      setTimeout(() => setToast(t => (t && t.msg === msg ? null : t)), 2200);
    },
    // CTA sticky inferior — la pantalla de detalle lo activa al montar,
    // lo limpia al desmontar (via useEffect cleanup).
    showCTA: (cta) => setBottomCTA(cta),
    hideCTA: () => setBottomCTA(null),
    joinReto, leaveReto, joinedRetos,
  }), [tweaks.joinedRetos]);
  useEf(() => { window.__pauNav = nav; }, [nav]);

  function renderTab() {
    switch (tab) {
      case 'home': return <HomeScreen theme={theme} isPremium={tweaks.isPremium} nav={nav}/>;
      case 'retos': return <RetosScreen theme={theme} isPremium={tweaks.isPremium} nav={nav}/>;
      case 'recetas': return <RecetasScreen theme={theme} isPremium={tweaks.isPremium} nav={nav}/>;
      case 'comunidad': return <ComunidadScreen theme={theme} nav={nav}/>;
      default: return null;
    }
  }

  const top = stack[stack.length - 1];
  function renderDetail(item) {
    if (!item) return null;
    if (item.kind === 'reto') return <DetalleRetoScreen retoId={item.id} theme={theme} isPremium={tweaks.isPremium} nav={nav} onComplete={() => setTweak('isPremium', true)}/>;
    if (item.kind === 'receta') return <DetalleRecetaScreen recetaId={item.id} theme={theme} nav={nav}/>;
    if (item.kind === 'plan') return <PlanDetailScreen planId={item.id} theme={theme} isPremium={tweaks.isPremium} nav={nav} onComplete={() => setTweak('isPremium', true)}/>;
    if (item.kind === 'rutina') return <RutinaDetailScreen rutinaId={item.id} theme={theme} isPremium={tweaks.isPremium} nav={nav}/>;
    if (item.kind === 'programa') return <ProgramaDetailScreen programaId={item.id} theme={theme} isPremium={tweaks.isPremium} nav={nav}/>;
    if (item.kind === 'logros') return <LogrosScreen theme={theme} nav={nav}/>;
    if (item.kind === 'calendario') return <CalendarioScreen retoId={item.id} theme={theme} nav={nav}/>;
    if (item.kind === 'progreso') return <ProgresoScreen theme={theme} nav={nav}/>;
    if (item.kind === 'historial') return <HistorialScreen theme={theme} nav={nav}/>;
    if (item.kind === 'perfil') return <PerfilScreen theme={theme} isPremium={tweaks.isPremium} isPau={tweaks.isPau} nav={nav}/>;
    if (item.kind === 'lista-compra') return <ListaCompraScreen theme={theme} nav={nav}/>;
    if (item.kind === 'ciclo') return <CicloScreen theme={theme} nav={nav}/>;
    if (item.kind === 'guardados') return <GuardadosScreen theme={theme} nav={nav}/>;
    if (item.kind === 'tienda') return <TiendaScreen theme={theme} nav={nav}/>;
    if (item.kind === 'login') return <LoginScreen theme={theme} nav={nav}/>;
    if (item.kind === 'onboarding') return <OnboardingScreen theme={theme} nav={nav}/>;
    if (item.kind === 'notif') return <NotificacionesScreen theme={theme} nav={nav}/>;
    if (item.kind === 'admin') return <AdminScreen theme={theme} nav={nav}/>;
    if (item.kind === 'ajustes') return <AjustesScreen theme={theme} nav={nav} tweaks={tweaks} setTweak={setTweak}/>;
    if (item.kind === 'editarPerfil') return <EditarPerfilScreen theme={theme} nav={nav}/>;
    if (item.kind === 'paywall') return <PaywallScreen theme={theme} nav={nav} onComplete={() => setTweak('isPremium', true)}/>;
    if (item.kind === 'ranking') return <RankingScreen theme={theme} nav={nav}/>;
    if (item.kind === 'chat') return <ChatScreen theme={theme} nav={nav}/>;
    if (item.kind === 'perfilPublico') return <PerfilPublicoScreen userId={item.id} theme={theme} nav={nav}/>;
    return null;
  }

  const fullScreenKinds = ['paywall', 'login', 'onboarding'];

  // Splash mientras carga sesión + datos
  if (!authChecked || !dataReady) {
    return (
      <IOSDevice dark={theme.dark} width={402} height={874}>
        <div className="pf-app" style={{
          position: 'absolute', inset: 0, background: theme.bg,
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 18,
        }}>
          <img src="/branding/logo.png" alt="Pau Fit" style={{
            width: 96, height: 96, objectFit: 'contain',
            animation: 'pulse 1.4s ease-in-out infinite',
          }}/>
          <div style={{
            width: 32, height: 32, borderRadius: 50,
            border: `2.5px solid ${theme.subtle}`,
            borderTopColor: theme.accent,
            animation: 'spin 800ms linear infinite',
          }}/>
        </div>
      </IOSDevice>
    );
  }

  // Si NO hay sesión → mostrar LoginScreen solo
  if (!session) {
    return (
      <IOSDevice dark={theme.dark} width={402} height={874}>
        <div className="pf-app" style={{ position: 'absolute', inset: 0, background: theme.bg, overflow: 'auto', color: theme.text }}>
          <LoginScreen theme={theme} nav={nav}/>
        </div>
        {toast && (
          <div style={{
            position: 'fixed', top: 'calc(env(safe-area-inset-top, 0px) + 20px)', left: '50%', transform: 'translateX(-50%)',
            zIndex: 99999, padding: '12px 20px', maxWidth: 'calc(100vw - 32px)',
            background: 'rgba(45,42,38,0.95)', color: '#fff',
            borderRadius: 999, boxShadow: '0 10px 30px rgba(0,0,0,0.3)',
            fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8,
            animation: 'slide-down 360ms cubic-bezier(.2,.8,.2,1)',
          }}>
            {toast.icon && <span style={{ fontSize: 16 }}>{toast.icon}</span>}
            {toast.msg}
          </div>
        )}
      </IOSDevice>
    );
  }

  return (
    <IOSDevice dark={theme.dark} width={402} height={874}>
      <div className="pf-app" style={{
        position: 'absolute', inset: 0, background: theme.bg,
        overflow: 'hidden', display: 'flex', flexDirection: 'column',
        color: theme.text,
      }}>
        <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
          <div style={{
            position: 'absolute', inset: 0,
            paddingTop: 'env(safe-area-inset-top, 0px)',
            overflowY: 'auto', overflowX: 'hidden',
            WebkitOverflowScrolling: 'touch',
          }} className="scroll-area" key={tab}>
            {renderTab()}
          </div>

          {top && (
            <div style={{
              position: 'absolute', inset: 0,
              background: theme.bg,
              overflowY: fullScreenKinds.includes(top.kind) ? 'hidden' : 'auto',
              overflowX: 'hidden',
              paddingTop: fullScreenKinds.includes(top.kind) ? 0 : 'env(safe-area-inset-top, 0px)',
              animation: transition === 'pop' ? 'slide-out-right 280ms cubic-bezier(.4,0,.2,1) forwards' :
                         'slide-in-right 340ms cubic-bezier(.2,.8,.2,1) forwards',
              boxShadow: '-10px 0 30px rgba(0,0,0,0.08)',
              zIndex: 30,
            }} className="scroll-area" key={`${top.kind}-${top.id}-${stack.length}`}>
              {renderDetail(top)}
            </div>
          )}
        </div>

        {/* CTA sticky inferior (estilo Sweat/BODi) — vive aquí, FUERA del
            scroll-area. Inmune a transforms de animaciones. Aparece justo
            encima del TabBar cuando una pantalla lo registra. */}
        {bottomCTA && !modal && (
          <div style={{
            position: 'absolute', bottom: bottomCTA.aboveTabBar ? 76 : 0,
            left: 0, right: 0,
            zIndex: 80,
            padding: '14px 18px calc(env(safe-area-inset-bottom, 0px) + 16px)',
            background: bottomCTA.transparent ? 'transparent' : (theme.dark
              ? 'linear-gradient(180deg, rgba(27,22,18,0) 0%, rgba(27,22,18,0.96) 30%)'
              : `linear-gradient(180deg, ${theme.bg}00 0%, ${theme.bg}f0 30%, ${theme.bg} 60%)`),
            backdropFilter: 'blur(8px)',
            WebkitBackdropFilter: 'blur(8px)',
            pointerEvents: 'auto',
          }}>
            <button
              onClick={bottomCTA.onClick}
              disabled={bottomCTA.disabled}
              style={{
                width: '100%', padding: '16px',
                border: 'none', borderRadius: 16, cursor: bottomCTA.disabled ? 'wait' : 'pointer',
                background: bottomCTA.disabled ? theme.subtle : (bottomCTA.color || theme.accent),
                color: bottomCTA.disabled ? theme.muted : '#fff',
                fontSize: 14, fontWeight: 700, letterSpacing: 0.3,
                boxShadow: bottomCTA.disabled ? 'none' : `0 8px 20px ${(bottomCTA.color || theme.accent)}55`,
                fontFamily: 'inherit',
                transition: 'transform 120ms ease, box-shadow 120ms ease',
                opacity: bottomCTA.disabled ? 0.7 : 1,
              }}
              onMouseDown={(e) => !bottomCTA.disabled && (e.currentTarget.style.transform = 'scale(0.98)')}
              onMouseUp={(e) => (e.currentTarget.style.transform = 'scale(1)')}
              onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
              onTouchStart={(e) => !bottomCTA.disabled && (e.currentTarget.style.transform = 'scale(0.98)')}
              onTouchEnd={(e) => (e.currentTarget.style.transform = 'scale(1)')}
            >
              {bottomCTA.label}
            </button>
          </div>
        )}

        {!(top && fullScreenKinds.includes(top.kind)) && !modal && (
          <TabBar active={tab} onChange={(t) => nav.tab(t)} dark={theme.dark} accent={theme.accent}/>
        )}

        {modal && (
          <div style={{ position: 'absolute', inset: 0, zIndex: 500 }}>{modal}</div>
        )}

        {toast && (
          <div style={{
            position: 'absolute', top: 60, left: '50%', transform: 'translateX(-50%)',
            zIndex: 700, padding: '10px 18px',
            background: theme.dark ? 'rgba(245,234,239,0.95)' : 'rgba(14,10,12,0.94)',
            color: theme.dark ? '#0B0709' : '#fff',
            borderRadius: 999, boxShadow: '0 10px 30px rgba(0,0,0,0.25)',
            fontSize: 13, fontWeight: 600,
            display: 'flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap',
            animation: 'slide-down 360ms cubic-bezier(.2,.8,.2,1)',
          }}>
            {toast.icon && <span style={{ fontSize: 16 }}>{toast.icon}</span>}
            {toast.msg}
          </div>
        )}
      </div>

      <TweaksPanel title="Tweaks">
        <TweakSection label="Apariencia">
          <TweakColor
            label="Paleta"
            value={tweaks.palette}
            options={PALETTE_META.map(p => p.swatches)}
            onChange={(v) => {
              const m = PALETTE_META.find(p => JSON.stringify(p.swatches) === JSON.stringify(v));
              if (m) setTweak('palette', m.id);
            }}
          />
          <TweakSelect
            label="Tipografía"
            value={tweaks.font}
            options={Object.entries(FONT_PRESETS).map(([id, p]) => ({ value: id, label: `${p.name} — ${p.hint}` }))}
            onChange={(v) => setTweak('font', v)}
          />
          <TweakToggle
            label="Modo oscuro"
            value={tweaks.darkMode}
            onChange={(v) => setTweak('darkMode', v)}
          />
        </TweakSection>
        <TweakSection label="Usuario">
          <TweakRadio
            label="Plan"
            value={tweaks.isPremium ? 'premium' : 'free'}
            options={[
              { value: 'free', label: 'Gratis' },
              { value: 'premium', label: 'Premium' },
            ]}
            onChange={(v) => setTweak('isPremium', v === 'premium')}
          />
          <TweakToggle
            label="Cuenta admin (Pau)"
            value={tweaks.isPau}
            onChange={(v) => setTweak('isPau', v)}
          />
        </TweakSection>
        <TweakSection label="Pantallas extra">
          <TweakButton label="Ver Onboarding" onClick={() => nav.go('onboarding')}/>
          <TweakButton label="Ver Paywall" secondary onClick={() => nav.go('paywall')}/>
        </TweakSection>
      </TweaksPanel>
    </IOSDevice>
  );
}

// Stage SIN frame: App llena 100% de la ventana, sin scale, sin radial gradient marrón.
// El "marco beige" que se veía antes era el background de este Stage cuando scale<1.
function Stage() {
  return <App/>;
}

// En la página v2 (app2.html) este archivo solo aporta los helpers window.*
// — el montaje lo hace src3/app2-shell.jsx.
if (!window.__PAU_V2) {
  ReactDOM.createRoot(document.getElementById('root')).render(<Stage/>);
}
