// AppStore — global state + localStorage persistence
const STORE_KEY = 'atlas.store.v2';

// Fresh, empty state for a brand-new user — no name, no history, 0 gems, 0 streak.
// (The old build shipped a hardcoded "Javier" demo with 12 sessions / streak 4 /
// 240 gems, which every fresh device inherited. That demo now lives in DEMO_STORE,
// gated behind ?demo=1, and is never loaded by default.)
const DEFAULT_STORE = {
  user: {
    name: '',
    level: 'intermedio',
    onboarded: false,
  },
  gems: {
    balance: 0,
    history: [],
  },
  // Lifetime experience = gross gems ever earned (spending never lowers it).
  // Drives the visible level in the profile. Seeded retroactively once (see
  // StoreProvider) so existing users don't start Phase 3 at zero.
  xp: 0,
  xpSeeded: false,
  favorites: [],
  reading: {
    progress: {},
    lastRead: null,
    completed: [],
  },
  plan: null,
  sessions: {
    completed: 0,
    streak: 0,
    lastDate: null,
    lastGemDate: null,          // last calendar day a session reward was granted (daily cap)
    lastWeeklyBonusWeek: null,  // week key of the last weekly-goal bonus (once/week)
  },
  log: [],
  savedRoutines: [],
  store: { owned: [] },
  achievements: [],
  // Equipped cosmetics (avatar + frame). Cosmetics are the ONLY thing gems buy;
  // owned cosmetic ids live in store.owned. 'default'/'none' are always free.
  cosmetics: { avatar: 'default', frame: 'none' },
  prefs: {
    theme: 'light',
    sinnerduo: true,
    timerSound: true,        // sound + vibration on the rest timer
    keepAwake: true,         // Wake Lock during training
    animateExercises: true,  // crossfade the two free-exercise-db positions
  },
  currentWorkout: [],
};

// Development-only demo data, loaded ONLY with ?demo=1 (persisted via the
// atlas.demo flag). Never seeded in production by default.
const DEMO_STORE = {
  ...structuredClone(DEFAULT_STORE),
  user: { name: 'Javier', level: 'intermedio', onboarded: false },
  gems: { balance: 240, history: [] },
  sessions: { completed: 12, streak: 4, lastDate: null },
};

// ?demo=1 turns on demo seeding (and remembers it); ?demo=0 turns it off.
function isDemoMode() {
  try {
    const q = new URLSearchParams(location.search);
    if (q.get('demo') === '1') { localStorage.setItem('atlas.demo', '1'); return true; }
    if (q.get('demo') === '0') { localStorage.removeItem('atlas.demo'); return false; }
    return localStorage.getItem('atlas.demo') === '1';
  } catch (e) { return false; }
}

// Detects the stale hardcoded demo seed that older builds persisted to a fresh
// device on first load. A real user's `sessions.completed` is only ever
// incremented by logSession, which pushes to `log` in the same update — so
// completed>0 with an empty log cannot be genuine user data. This never matches
// a real account (they always have matching log entries).
function isStaleDemoSeed(p) {
  if (!p) return false;
  const completed = p?.sessions?.completed || 0;
  const logLen = Array.isArray(p?.log) ? p.log.length : 0;
  if (completed > 0 && logLen === 0) return true;
  // Belt: the exact legacy identity with no real activity of any kind.
  if (p?.user?.name === 'Javier' && logLen === 0 &&
      (p?.savedRoutines?.length || 0) === 0 && !(p?.gems?.history?.length)) return true;
  return false;
}

// One-time migration: fold sessions that exist only in the legacy
// atlas.sessionhistory.v1 store (written by the old wsComplete) into store.log,
// which is the single canonical session history. Legacy player sessions were
// double-written with different timestamps (start vs finish), so dedupe by
// calendar date + exercise-name overlap rather than by exact dateTs.
function migrateLegacyHistory(log) {
  const LEGACY_KEY = 'atlas.sessionhistory.v1';
  try {
    const raw = localStorage.getItem(LEGACY_KEY);
    if (!raw) return log;
    const legacy = JSON.parse(raw);
    if (!Array.isArray(legacy) || !legacy.length) {
      localStorage.removeItem(LEGACY_KEY);
      return log;
    }
    const isDupe = (s) => log.some(e => {
      if (e.date !== s.date) return false;
      const names = new Set((e.exercises || []).map(x => x.name));
      const sNames = (s.exercises || []).map(x => x.name);
      if (!sNames.length) return true;
      const overlap = sNames.filter(n => names.has(n)).length;
      return overlap >= Math.ceil(sNames.length * 0.5);
    });
    const converted = legacy
      .filter(s => s && s.dateTs && !isDupe(s))
      .map(s => ({
        id: s.dateTs,
        date: s.date || new Date(s.dateTs).toDateString(),
        dateTs: s.dateTs,
        exercises: (s.exercises || []).map(ex => ({
          name: ex.name,
          muscles: Array.isArray(ex.muscles) ? ex.muscles : (ex.muscles?.primary || []),
          sets: (ex.sets || []).filter(st => st.done).map(st => ({ kg: st.kg, reps: st.reps })),
        })),
        gems: 0,
        ...(s.duration ? { duration: s.duration } : {}),
        ...(s.routineName ? { routineName: s.routineName } : {}),
        ...(s.sessionName ? { sessionName: s.sessionName } : {}),
      }));
    localStorage.removeItem(LEGACY_KEY);
    if (!converted.length) return log;
    return [...log, ...converted]
      .sort((a, b) => (b.dateTs || 0) - (a.dateTs || 0))
      .slice(0, 100);
  } catch (e) {
    return log;
  }
}

function readStore() {
  try {
    const demo = isDemoMode();
    const raw = localStorage.getItem(STORE_KEY);
    let merged;
    // Fresh device, or a device still carrying the old hardcoded demo seed →
    // start empty (or load the demo data only when ?demo=1 is set).
    let parsed = null;
    try { parsed = raw ? JSON.parse(raw) : null; } catch (e) { parsed = null; }
    if (!demo && parsed && isStaleDemoSeed(parsed)) {
      try { localStorage.removeItem(STORE_KEY); } catch (e) {}
      parsed = null;
    }
    if (!parsed) {
      merged = structuredClone(demo ? DEMO_STORE : DEFAULT_STORE);
    } else {
      // Deep merge to handle new fields
      merged = structuredClone(DEFAULT_STORE);
      if (parsed.user) merged.user = { ...merged.user, ...parsed.user };
      if (parsed.gems) merged.gems = { ...merged.gems, ...parsed.gems };
      if (parsed.xp !== undefined) merged.xp = parsed.xp;
      if (parsed.xpSeeded !== undefined) merged.xpSeeded = parsed.xpSeeded;
      if (parsed.favorites) merged.favorites = parsed.favorites;
      if (parsed.reading) merged.reading = { ...merged.reading, ...parsed.reading };
      if (parsed.plan !== undefined) merged.plan = parsed.plan;
      if (parsed.sessions) merged.sessions = { ...merged.sessions, ...parsed.sessions };
      if (parsed.log) merged.log = parsed.log;
      if (parsed.savedRoutines) merged.savedRoutines = parsed.savedRoutines;
      if (parsed.store) merged.store = { ...merged.store, ...parsed.store };
      if (parsed.achievements) merged.achievements = parsed.achievements;
      if (parsed.cosmetics) merged.cosmetics = { ...merged.cosmetics, ...parsed.cosmetics };
      if (parsed.prefs) merged.prefs = { ...merged.prefs, ...parsed.prefs };
      if (parsed.currentWorkout) merged.currentWorkout = parsed.currentWorkout;
    }
    merged.log = migrateLegacyHistory(merged.log);
    // Backfill training achievements for existing history on load (no
    // celebration — the UI only celebrates ids unlocked after this point).
    try { if (window.evaluateAchievements) merged.achievements = window.evaluateAchievements(merged); } catch (e) {}
    // Note: retroactive XP is seeded once in StoreProvider (a post-mount effect),
    // not here — readStore can run before the engine helpers are defined.
    return merged;
  } catch (e) {
    return structuredClone(DEFAULT_STORE);
  }
}

function writeStore(s) {
  try { localStorage.setItem(STORE_KEY, JSON.stringify(s)); } catch (e) {}
}

const StoreContext = React.createContext(null);

function StoreProvider({ children }) {
  const [state, setState] = React.useState(() => readStore());

  React.useEffect(() => { writeStore(state); }, [state]);

  // One-time retroactive XP seed. Runs after mount, when the engine helpers are
  // defined. Existing users get XP from their durable history; new users keep 0.
  // Guarded by `xpSeeded` so it never re-seeds or overwrites earned XP.
  React.useEffect(() => {
    setState(s => {
      if (s.xpSeeded) return s;
      let retro = 0;
      try { retro = window.computeRetroXP ? window.computeRetroXP(s) : 0; } catch (e) {}
      return { ...s, xp: Math.max(s.xp || 0, retro), xpSeeded: true };
    });
  }, []);

  const actions = React.useMemo(() => ({
    setUser: (patch) => setState(s => ({ ...s, user: { ...s.user, ...patch } })),

    spendGems: (amount, reason) => setState(s => {
      if (s.gems.balance < amount) return s;
      return {
        ...s,
        gems: {
          balance: s.gems.balance - amount,
          history: [{ id: Date.now(), amount: -amount, reason, ts: Date.now() }, ...s.gems.history].slice(0, 50),
        },
      };
    }),

    toggleFavorite: (articleId) => setState(s => ({
      ...s,
      favorites: s.favorites.includes(articleId)
        ? s.favorites.filter(id => id !== articleId)
        : [...s.favorites, articleId],
    })),

    setReadingProgress: (articleId, ratio) => setState(s => ({
      ...s,
      reading: {
        ...s.reading,
        lastRead: articleId,
        progress: { ...s.reading.progress, [articleId]: Math.max(s.reading.progress[articleId] || 0, ratio) },
      },
    })),

    savePlan: (plan) => setState(s => ({ ...s, plan })),

    // Log a workout session with exercises.
    // Gems are tiered by real effort and computed by the caller (the player):
    // meta.gemsEarned is 0/10/20. Builder logs pass no gems. The reward is
    // capped to one session reward per calendar day (sessions.lastGemDate).
    // The log entry is always recorded (so it counts toward the weekly goal and
    // achievements), regardless of whether gems were granted.
    logSession: (exercises, meta) => setState(s => {
      const today = new Date().toDateString();
      const wasYesterday = s.sessions.lastDate === new Date(Date.now() - 86400000).toDateString();
      const alreadyToday = s.sessions.lastDate === today;
      const newStreak = alreadyToday
        ? s.sessions.streak
        : wasYesterday
          ? s.sessions.streak + 1
          : 1;

      const now = Date.now();

      // 1) Session reward — effort-tiered, capped to one per calendar day.
      const tier = Math.max(0, Math.round(meta?.gemsEarned || 0));
      const rewardedToday = s.sessions.lastGemDate === today;
      const sessionAward = (!rewardedToday && tier > 0) ? tier : 0;

      const entry = {
        id: now,
        date: today,
        dateTs: now,
        exercises,
        gems: sessionAward,
        ...(meta?.duration ? { duration: meta.duration } : {}),
        ...(meta?.routineName ? { routineName: meta.routineName } : {}),
        ...(meta?.sessionName ? { sessionName: meta.sessionName } : {}),
        ...(meta?.partial ? { partial: true } : {}),
      };

      const newLog = [entry, ...s.log].slice(0, 100);
      const newCompleted = s.sessions.completed + (alreadyToday ? 0 : 1);

      // 2) Weekly-goal bonus — +40 once per Mon–Sun week the goal is met.
      //    Counts Builder sessions too (any logged session counts toward days).
      const goal = window.atlasWeeklyGoal ? window.atlasWeeklyGoal() : 3;
      const wk = window.computeActiveWeeks ? window.computeActiveWeeks(newLog, goal) : null;
      const weeklyAward = (wk && wk.metCurrentWeek && s.sessions.lastWeeklyBonusWeek !== wk.currentWeekKey) ? 40 : 0;

      // 3) Personal-record bonus — player sessions only (meta.allowPR), +10 each,
      //    max +20, compared against prior history (first-ever logs never count).
      const prAward = (meta?.allowPR && window.computeSessionPRBonus)
        ? window.computeSessionPRBonus(s.log, exercises) : 0;

      // Flag the session for proactive analysis next time Atlas Coach opens
      try {
        localStorage.setItem('atlas.coach.pendingAnalysis.v1', JSON.stringify({ dateTs: entry.dateTs }));
      } catch (e) {}

      const nextState = {
        ...s,
        log: newLog,
        sessions: {
          completed: newCompleted,
          streak: newStreak,
          lastDate: today,
          lastGemDate: sessionAward > 0 ? today : s.sessions.lastGemDate,
          lastWeeklyBonusWeek: weeklyAward > 0 ? wk.currentWeekKey : s.sessions.lastWeeklyBonusWeek,
        },
      };
      // Recompute all achievements from the new state.
      const newAchievements = window.evaluateAchievements
        ? window.evaluateAchievements(nextState)
        : s.achievements;

      // Assemble gem awards as separate history entries (each surfaces its own
      // "+N 💎 · motivo" toast). Newest-first in the history list.
      const rewards = [];
      if (sessionAward > 0) rewards.push({ amount: sessionAward, reason: 'Sesión completada' });
      if (prAward > 0)      rewards.push({ amount: prAward, reason: prAward >= 20 ? 'Marcas personales' : 'Marca personal' });
      if (weeklyAward > 0)  rewards.push({ amount: weeklyAward, reason: 'Objetivo semanal' });
      const totalAward = sessionAward + prAward + weeklyAward;
      const newHistory = rewards.length
        ? [...rewards.map((rw, i) => ({ id: now + i + 1, amount: rw.amount, reason: rw.reason, ts: now })).reverse(), ...s.gems.history].slice(0, 50)
        : s.gems.history;

      return {
        ...nextState,
        gems: totalAward > 0 ? { balance: s.gems.balance + totalAward, history: newHistory } : s.gems,
        xp: (s.xp || 0) + totalAward,   // XP = gross gems earned (never spent down)
        achievements: newAchievements,
      };
    }),

    // ── Saved routines ("Mis rutinas") — single source of truth ──────────────
    // Save (or update) a named routine template. If a routine with the same id
    // already exists it is replaced; otherwise it is prepended (cap 50).
    saveRoutine: (routine) => setState(s => {
      if (!routine) return s;
      const id = routine.id || `routine-${Date.now()}`;
      const entry = { ...routine, id, savedAt: Date.now() };
      const existing = (s.savedRoutines || []).some(r => r.id === id);
      const savedRoutines = existing
        ? s.savedRoutines.map(r => r.id === id ? { ...entry, createdAt: r.createdAt || entry.createdAt } : r)
        : [entry, ...(s.savedRoutines || [])].slice(0, 50);
      return { ...s, savedRoutines };
    }),

    renameRoutine: (id, name) => setState(s => ({
      ...s,
      savedRoutines: (s.savedRoutines || []).map(r =>
        r.id === id ? { ...r, name: name || r.name, savedAt: Date.now() } : r),
    })),

    duplicateRoutine: (id) => setState(s => {
      const orig = (s.savedRoutines || []).find(r => r.id === id);
      if (!orig) return s;
      const copy = {
        ...orig,
        id: `routine-${Date.now()}`,
        name: `${orig.name} (copia)`,
        createdAt: Date.now(),
        savedAt: Date.now(),
      };
      const idx = s.savedRoutines.findIndex(r => r.id === id);
      const next = [...s.savedRoutines];
      next.splice(idx + 1, 0, copy);
      return { ...s, savedRoutines: next.slice(0, 50) };
    }),

    deleteRoutine: (id) => setState(s => ({
      ...s,
      savedRoutines: (s.savedRoutines || []).filter(r => r.id !== id),
    })),

    // Mark an article as read; award a fixed 15 gems the first time only.
    // Reading achievements (primer-articulo, cinco-articulos) are derived
    // centrally by evaluateAchievements.
    markArticleRead: (id) => setState(s => {
      const alreadyRead = (s.reading.completed || []).includes(id);
      const newCompleted = alreadyRead
        ? s.reading.completed
        : [...(s.reading.completed || []), id];

      const nextState = {
        ...s,
        reading: { ...s.reading, lastRead: id, completed: newCompleted },
      };
      const newAchievements = window.evaluateAchievements
        ? window.evaluateAchievements(nextState)
        : s.achievements;

      return {
        ...nextState,
        gems: alreadyRead ? s.gems : {
          balance: s.gems.balance + 15,
          history: [{ id: Date.now(), amount: 15, reason: 'Artículo leído', ts: Date.now() }, ...s.gems.history].slice(0, 50),
        },
        xp: (s.xp || 0) + (alreadyRead ? 0 : 15),
        achievements: newAchievements,
      };
    }),

    // NEW: Buy a store item
    buyStoreItem: (id, cost) => setState(s => {
      if (s.gems.balance < cost) return s;
      if ((s.store.owned || []).includes(id)) return s;
      return {
        ...s,
        store: { owned: [...(s.store.owned || []), id] },
        gems: {
          balance: s.gems.balance - cost,
          history: [{ id: Date.now(), amount: -cost, reason: `Compra: ${id}`, ts: Date.now() }, ...s.gems.history].slice(0, 50),
        },
      };
    }),

    // Equip an owned (or free) cosmetic. kind is 'avatar' | 'frame'.
    equipCosmetic: (kind, id) => setState(s => ({
      ...s,
      cosmetics: { ...(s.cosmetics || { avatar: 'default', frame: 'none' }), [kind]: id },
    })),

    // NEW: Unlock an achievement
    unlockAchievement: (id) => setState(s => {
      if (s.achievements.includes(id)) return s;
      return { ...s, achievements: [...s.achievements, id] };
    }),

    // Save a generated plan. No gems (saving isn't progress); instead it unlocks
    // the one-time "Primer plan creado" achievement, derived by evaluateAchievements.
    saveProtocol: (protocol) => setState(s => {
      const nextState = { ...s, plan: protocol };
      const newAchievements = window.evaluateAchievements
        ? window.evaluateAchievements(nextState)
        : s.achievements;
      return { ...nextState, achievements: newAchievements };
    }),

    setCurrentWorkout: (exercises) => setState(s => ({ ...s, currentWorkout: exercises })),

    setPref: (key, value) => setState(s => ({ ...s, prefs: { ...s.prefs, [key]: value } })),

    reset: () => setState(structuredClone(DEFAULT_STORE)),
  }), []);

  return (
    <StoreContext.Provider value={{ state, actions }}>
      {children}
    </StoreContext.Provider>
  );
}

function useStore() {
  const ctx = React.useContext(StoreContext);
  if (!ctx) throw new Error('useStore must be inside StoreProvider');
  return ctx;
}

// ─── Hash router ──────────────────────────────────────
const ROUTES = ['/', '/aula', '/aula/glosario', '/laboratorio', '/coach', '/coach-online', '/builder', '/rutinas', '/plantillas', '/perfil', '/ajustes', '/tienda', '/admin', '/player', '/progreso', '/legal', '/privacidad', '/cookies', '/terminos', '/creditos'];

  // Hash shape: "#/path" or "#/path#fragment" (fragment = deep-link anchor, e.g.
  // a glossary term). The route is only the path part.
  const pathOf = (raw) => {
    const s = (raw || '').replace(/^#/, '');
    const i = s.indexOf('#');
    const p = (i >= 0 ? s.slice(0, i) : s) || '/';
    return ROUTES.includes(p) ? p : '/';
  };

function useRoute() {
  const [route, setRoute] = React.useState(() => pathOf(window.location.hash));

  React.useEffect(() => {
    let prev = pathOf(window.location.hash);
    const onHash = () => {
      const next = pathOf(window.location.hash);
      setRoute(next);
      // Only reset scroll when the PATH changes — an in-page fragment change
      // (deep-linking to a glossary term) must keep the anchor scroll.
      if (next !== prev) window.scrollTo({ top: 0, behavior: 'instant' });
      prev = next;
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  const navigate = React.useCallback((path) => {
    window.location.hash = path;
  }, []);

  return { route, navigate };
}

// Accessibility helper: close a modal/sheet with the Escape key.
function useEscClose(onClose, active = true) {
  React.useEffect(() => {
    if (!active || typeof onClose !== 'function') return;
    const h = (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } };
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [onClose, active]);
}

Object.assign(window, { StoreProvider, useStore, useRoute, useEscClose });
