// Atlas Coach v3 — Intelligent training assistant with persistent memory

// ── Design tokens ─────────────────────────────────────────────────────────────
const AC = {
  page:    '#060D18',
  sidebar: '#06111F',
  card:    '#0E1A2C',
  card2:   '#0A1628',
  hover:   '#132134',
  border:  'rgba(255,255,255,0.07)',
  text:    '#E8EDF8',
  sub:     'rgba(232,237,248,0.55)',
  muted:   'rgba(232,237,248,0.55)',
  blue:    '#3B82F6',
  green:   '#22C55E',
  amber:   '#F59E0B',
  red:     '#EF4444',
  input:   '#0A1422',
};

// Enlace de alcance: cuando el Coach no puede ayudar (técnica, lesión, dieta,
// seguimiento personal) ofrece la página "Entrena con un coach". El texto del
// mensaje debe contener literalmente la etiqueta para que se renderice como
// enlace in-line (ver acRenderTextWithLink / AcCoachMessage).
const AC_COACH_LINK = { label: 'entrenar con un coach', to: '/coach-online' };

// Servicio de coaching personalizado (flag). Con el flag desactivado, el Coach
// no ofrece ni enlaza el servicio: recomienda acudir a un profesional titulado.
function acCoachOnlineOn() {
  return (typeof ATLAS_FEATURE === 'function') && ATLAS_FEATURE('FEATURE_COACH_ONLINE');
}
// Frase que sustituye a la oferta del servicio cuando el flag está desactivado.
const AC_PRO_REF = 'Si quieres un plan a medida o que alguien revise tu técnica, lo recomendable es acudir a un profesional titulado en Ciencias de la Actividad Física y del Deporte.';
// Enlace solo cuando el servicio está activo (si no, sin enlace).
function acCoachLink() { return acCoachOnlineOn() ? AC_COACH_LINK : undefined; }

// Atlas Coach beta flag — the single switch. When the AI version ships, set this
// to false to remove the "Beta" label and the beta notice everywhere in the
// Coach (and nowhere else in the app). Nothing else needs to change.
const AC_BETA = true;

// ── Exercise grouping ─────────────────────────────────────────────────────────
// Coach split group of an exercise — reads the authoritative `muscleGroup`
// (separado del patrón de movimiento). Mantiene el reparto cuádriceps→piernas /
// cadena posterior→glúteos que usan los splits. Fallback al patrón por si faltara.
function acExGroup(ex) {
  const mg = ex.muscleGroup;
  if (mg) {
    switch (mg) {
      case 'pecho': return 'pecho';
      case 'espalda': case 'espalda-baja': case 'trapecio': return 'espalda';
      case 'delt_ant': case 'delt_lat': case 'delt_post': return 'hombro';
      case 'biceps': case 'antebrazo': return 'biceps';
      case 'triceps': return 'triceps';
      case 'cuadriceps': case 'gemelos': case 'tibial': return 'piernas';
      case 'isquios': case 'gluteos': case 'abductores': case 'aductores': return 'gluteos';
      case 'core': return 'core';
      default: return 'core';
    }
  }
  const p = ex.pattern || '';
  if (p === 'empuje-horizontal') return 'pecho';
  if (p === 'empuje-vertical')
    return (ex.muscles?.primary?.[0] || '').includes('Tríceps') ? 'triceps' : 'hombro';
  if (p === 'traccion-horizontal')
    return (ex.muscles?.primary?.[0] || '').includes('Deltoides') ? 'hombro' : 'espalda';
  if (p === 'traccion-vertical') {
    const pm = ex.muscles?.primary?.[0] || '';
    return (pm.includes('Bíceps') || pm.includes('Braquial')) ? 'biceps' : 'espalda';
  }
  if (p === 'sentadilla' || p === 'aislamiento-pantorrilla') return 'piernas';
  if (p === 'bisagra') return 'gluteos';
  if (p.startsWith('core')) return 'core';
  return 'core';
}

// ── Storage helpers ───────────────────────────────────────────────────────────
const AC_PROFILE_KEY = 'atlas.coach.profile.v1';
const AC_MEMORY_KEY  = 'atlas.coach.memory.v1';
const AC_CHATS_KEY   = 'atlas.chats.v1';

function acLoadProfile() {
  try { return JSON.parse(localStorage.getItem(AC_PROFILE_KEY) || 'null'); } catch { return null; }
}
function acSaveProfile(p) {
  try { localStorage.setItem(AC_PROFILE_KEY, JSON.stringify({ ...p, updatedAt: Date.now() })); } catch {}
}
function acLoadMemory() {
  try {
    return JSON.parse(localStorage.getItem(AC_MEMORY_KEY) || 'null') ||
      { lesiones: [], evitados: [], notas: [] };
  } catch { return { lesiones: [], evitados: [], notas: [] }; }
}
function acSaveMemory(m) {
  try { localStorage.setItem(AC_MEMORY_KEY, JSON.stringify({ ...m, updatedAt: Date.now() })); } catch {}
}

// ── Atlas Profile storage ─────────────────────────────────────────────────────
const AP_PROFILE_KEY   = 'atlas.profile.v1';
const AP_DISMISSED_KEY = 'atlas.profile.dismissed';

function apLoadProfile() {
  try { return JSON.parse(localStorage.getItem(AP_PROFILE_KEY) || 'null'); } catch { return null; }
}
function apSaveProfile(p) {
  try { localStorage.setItem(AP_PROFILE_KEY, JSON.stringify({ ...p, completedAt: Date.now() })); } catch {}
}
function apIsDismissed() {
  return localStorage.getItem(AP_DISMISSED_KEY) === 'true';
}
function apSetDismissed() {
  try { localStorage.setItem(AP_DISMISSED_KEY, 'true'); } catch {}
}

// ── Onboarding steps ──────────────────────────────────────────────────────────
const AP_STEPS = [
  {
    id: 'objective',
    question: '¿Cuál es tu objetivo principal?',
    options: [
      { v: 'muscle',      l: 'Ganar masa muscular'    },
      { v: 'fat_loss',    l: 'Perder grasa'           },
      { v: 'recomp',      l: 'Recomposición corporal' },
      { v: 'performance', l: 'Rendimiento deportivo'  },
      { v: 'health',      l: 'Salud y bienestar'      },
    ],
    multi: false,
  },
  {
    id: 'experience',
    question: '¿Cuál es tu nivel de experiencia?',
    options: [
      { v: 'beginner',     l: 'Principiante' },
      { v: 'intermediate', l: 'Intermedio'   },
      { v: 'advanced',     l: 'Avanzado'     },
    ],
    multi: false,
  },
  {
    id: 'trainingDays',
    question: '¿Cuántos días entrenas por semana?',
    options: [
      { v: 2, l: '2 días' },
      { v: 3, l: '3 días' },
      { v: 4, l: '4 días' },
      { v: 5, l: '5 días' },
      { v: 6, l: '6+ días' },
    ],
    multi: false,
  },
  {
    id: 'sessionDuration',
    question: '¿Cuánto tiempo tienes por sesión?',
    options: [
      { v: 30, l: '30 min'  },
      { v: 45, l: '45 min'  },
      { v: 60, l: '60 min'  },
      { v: 90, l: '90+ min' },
    ],
    multi: false,
  },
  {
    id: 'equipment',
    question: '¿Qué equipamiento tienes disponible?',
    options: [
      { v: 'full_gym',      l: 'Gimnasio completo' },
      { v: 'basic_gym',     l: 'Gimnasio básico'   },
      { v: 'home',          l: 'Casa'              },
      { v: 'calisthenics',  l: 'Calistenia'        },
      { v: 'mixed',         l: 'Mixto'             },
    ],
    multi: false,
  },
  {
    id: 'musclePriorities',
    question: 'Selecciona tus prioridades musculares (hasta 3):',
    options: [
      { v: 'chest',     l: 'Pecho'      },
      { v: 'back',      l: 'Espalda'    },
      { v: 'shoulders', l: 'Hombros'    },
      { v: 'arms',      l: 'Brazos'     },
      { v: 'legs',      l: 'Piernas'    },
      { v: 'glutes',    l: 'Glúteos'    },
      { v: 'calves',    l: 'Gemelos'    },
      { v: 'forearms',  l: 'Antebrazos' },
    ],
    multi: true,
    maxSelect: 3,
  },
];

// ── Onboarding Level 2 steps (optional) ──────────────────────────────────────
const AP_STEPS_L2 = [
  {
    id: 'sex',
    question: '¿Cuál es tu sexo?',
    options: [
      { v: 'male',   l: 'Hombre'              },
      { v: 'female', l: 'Mujer'               },
      { v: 'other',  l: 'Prefiero no indicar' },
    ],
    multi: false,
  },
  {
    id: 'age',
    question: '¿Cuántos años tienes?',
    inputType: 'number',
    placeholder: 'Tu edad',
    unit: 'años',
    min: 10, max: 99,
    skippable: true,
    onDeviceNote: true,
  },
  {
    id: 'bodyMetrics',
    question: '¿Cuáles son tu altura y peso aproximados?',
    inputType: 'body-metrics',
    skippable: true,
    onDeviceNote: true,
  },
  {
    id: 'injuries',
    question: '¿Tienes alguna lesión o limitación física actual?',
    options: [
      { v: 'shoulder',   l: 'Hombro'         },
      { v: 'knee',       l: 'Rodilla'         },
      { v: 'lower_back', l: 'Espalda lumbar'  },
      { v: 'elbow',      l: 'Codo'            },
      { v: 'wrist',      l: 'Muñeca'          },
      { v: 'ankle',      l: 'Tobillo'         },
      { v: 'neck',       l: 'Cuello'          },
      { v: 'none',       l: 'Ninguna'         },
    ],
    multi: true,
    maxSelect: 7,
    hasNoteField: true,
    noneOption: 'none',
    skippable: true,
    onDeviceNote: true,
  },
  {
    id: 'previousInjuries',
    question: '¿Has tenido lesiones importantes en el pasado?',
    options: [
      { v: 'shoulder_past',  l: 'Hombro'        },
      { v: 'knee_past',      l: 'Rodilla'        },
      { v: 'back_past',      l: 'Espalda lumbar' },
      { v: 'elbow_past',     l: 'Codo'           },
      { v: 'wrist_past',     l: 'Muñeca'         },
      { v: 'ankle_past',     l: 'Tobillo'        },
      { v: 'none',           l: 'Sin antecedentes relevantes' },
    ],
    multi: true,
    maxSelect: 6,
    noneOption: 'none',
    skippable: true,
    onDeviceNote: true,
  },
  {
    id: 'avoidExercises',
    question: '¿Hay ejercicios que prefieres evitar o que te causan molestias?',
    inputType: 'text',
    placeholder: 'Ej: Sentadilla profunda, press de banca con barra…',
    skippable: true,
    onDeviceNote: true,
  },
  {
    id: 'activityLevel',
    question: '¿Cuál es tu nivel de actividad diaria?',
    options: [
      { v: 'sedentary',   l: 'Sedentario'           },
      { v: 'moderate',    l: 'Moderadamente activo'  },
      { v: 'active',      l: 'Activo'               },
      { v: 'very_active', l: 'Muy activo'            },
    ],
    multi: false,
  },
  {
    id: 'mainObstacle',
    question: '¿Qué es lo que más te dificulta progresar actualmente?',
    options: [
      { v: 'knowledge',   l: 'Falta de conocimientos' },
      { v: 'consistency', l: 'Falta de constancia'    },
      { v: 'time',        l: 'Falta de tiempo'        },
      { v: 'planning',    l: 'No sé cómo planificar'  },
      { v: 'results',     l: 'No veo resultados'      },
      { v: 'motivation',  l: 'Falta de motivación'    },
    ],
    multi: false,
  },
];

// Injected between L1 and L2 to offer optional continuation
const AP_L2_OFFER = {
  id: 'l2-offer',
  question: '¿Quieres mejorar aún más la precisión de las recomendaciones?',
  options: [
    { v: 'yes', l: 'Añadir información adicional' },
    { v: 'no',  l: 'Finalizar por ahora'          },
  ],
  multi: false,
  isL2Offer: true,
};

// ── Atlas Profile translation maps ───────────────────────────────────────────
const AP_OBJ_MAP  = { muscle:'hipertrofia', fat_loss:'pérdida de grasa', recomp:'recomposición corporal', performance:'rendimiento deportivo', health:'salud y bienestar' };
const AP_EXP_MAP  = { beginner:'principiante', intermediate:'intermedio', advanced:'avanzado' };
const AP_EQP_MAP  = { full_gym:'gimnasio completo', basic_gym:'gimnasio básico', home:'casa', calisthenics:'calistenia', mixed:'mixto' };
const AP_MUS_MAP  = { chest:'pecho', back:'espalda', shoulders:'hombros', arms:'brazos', legs:'piernas', glutes:'glúteos', calves:'gemelos', forearms:'antebrazos' };
const AP_DAYS_SPLIT = { 2:'upper_lower', 3:'ppl', 4:'upper_lower', 5:'ppl', 6:'ppl' };

// Aula-chip titles are resolved live from the real published articles / glossary
// (see AcAulaChips) — no hardcoded title map, so labels never drift or leak ids.


const AR_KEY      = 'atlas.routine.active.v1';
const AR_META_KEY = 'atlas.pendingWorkoutMeta';

function arSave(routine) {
  try { localStorage.setItem(AR_KEY, JSON.stringify({ ...routine, updatedAt: Date.now() })); } catch {}
}
function arLoad() {
  try { return JSON.parse(localStorage.getItem(AR_KEY) || 'null'); } catch { return null; }
}

// Per-injury: which muscle groups to cap in the routine builder
const AP_INJURY_GROUP_CAPS = {
  shoulder:   { hombro: 1 },
  knee:       { piernas: 1 },
  lower_back: {},
  elbow:      {},
  wrist:      {},
  ankle:      { piernas: 1 },
  neck:       { hombro: 1 },
};

// Per-injury: plain-text warnings injected into routine intro
const AP_INJURY_WARNS = {
  shoulder:   'He limitado el trabajo de hombro sobre la cabeza por tu limitación registrada.',
  knee:       'He reducido el rango y volumen de sentadilla por tu rodilla.',
  lower_back: 'He sustituido peso muerto convencional por variantes de menor riesgo para tu espalda lumbar.',
  elbow:      'He evitado extensiones y curls con agarre pronado por tu codo.',
  wrist:      'Uso mancuernas o agarre neutro en lugar de barra recta por tu muñeca.',
  ankle:      'He evitado prensa y sentadilla completa por tu tobillo.',
  neck:       'He eliminado movimientos con carga directa sobre el cuello.',
};

// Merges atlas.profile.v1 (English keys) into the shape the engine expects
function acMergeAtlasProfile(atlasProfile, coachProfile) {
  if (!atlasProfile) return coachProfile || null;
  const activeInjuries = (atlasProfile.injuries || []).filter(i => i !== 'none');
  const pastInjuries   = (atlasProfile.previousInjuries || []).filter(i => i !== 'none');
  return {
    // Legacy engine keys (Spanish)
    objetivo:     AP_OBJ_MAP[atlasProfile.objective]  || coachProfile?.objetivo     || 'hipertrofia',
    nivel:        AP_EXP_MAP[atlasProfile.experience] || coachProfile?.nivel        || 'intermedio',
    dias:         atlasProfile.trainingDays            || coachProfile?.dias         || 3,
    tiempo:       atlasProfile.sessionDuration         || coachProfile?.tiempo       || 60,
    equipamiento: AP_EQP_MAP[atlasProfile.equipment]  || coachProfile?.equipamiento || 'gimnasio',
    // New keys passed through
    objective:        atlasProfile.objective,
    experience:       atlasProfile.experience,
    musclePriorities: atlasProfile.musclePriorities || [],
    injuries:         activeInjuries,
    previousInjuries: pastInjuries,
    avoidExercises:   atlasProfile.avoidExercises || null,
    activityLevel:    atlasProfile.activityLevel   || null,
    mainObstacle:     atlasProfile.mainObstacle    || null,
    sex:              atlasProfile.sex             || null,
    age:              atlasProfile.age             || null,
    height:           atlasProfile.height          || null,
    weight:           atlasProfile.weight          || null,
  };
}

// ── Rich context builder ──────────────────────────────────────────────────────
// Always read the freshest history available: localStorage may be ahead of the
// React state (session saved from another view) or behind it (state updated
// but the persistence effect hasn't flushed yet) — pick whichever is newest.
function acLiveLog(state) {
  const inState = state?.log || [];
  let stored = [];
  try {
    const s = JSON.parse(localStorage.getItem('atlas.store.v2') || 'null');
    if (s && Array.isArray(s.log)) stored = s.log;
  } catch (e) {}
  if (!stored.length) return inState;
  if (!inState.length) return stored;
  return (stored[0].dateTs || 0) >= (inState[0].dateTs || 0) ? stored : inState;
}

function acBuildRichContext(state, profile) {
  const log      = acLiveLog(state);
  const sessions = state.sessions || {};
  const now      = Date.now();
  const weekAgo  = now - 7  * 86400000;
  const monthAgo = now - 30 * 86400000;

  const weekSessions  = log.filter(s => s.dateTs > weekAgo);
  const monthSessions = log.filter(s => s.dateTs > monthAgo);

  const daysSinceLast = log[0]?.dateTs
    ? Math.floor((now - log[0].dateTs) / 86400000)
    : 99;

  // Volume per muscle this week
  const weekMuscleVol = {};
  weekSessions.forEach(s => {
    (s.exercises || []).forEach(ex => {
      (ex.muscles || []).forEach(m => {
        const k = m.toLowerCase();
        weekMuscleVol[k] = (weekMuscleVol[k] || 0) + (ex.sets?.length || 1);
      });
    });
  });

  const sumVol = re => Object.entries(weekMuscleVol)
    .reduce((t, [k, v]) => t + (re.test(k) ? v : 0), 0);

  const pushVol = sumVol(/pectoral|tr[íi]ceps|deltoides ant/);
  const pullVol = sumVol(/dorsal|b[íi]ceps|romboides|braquial|trapecio medio/);
  const legVol  = sumVol(/cu[áa]driceps|isquios|gl[úu]teos|gemelo/);
  const coreVol = sumVol(/core|transverso|oblicuo|recto abdominal/);
  const pushPullRatio = pullVol > 0 ? pushVol / pullVol : pushVol > 0 ? 99 : 1;

  // Load progression: track max kg per exercise across last 8 sessions
  const recentLog = log.slice(0, 8);
  const exerciseKgHistory = {};
  recentLog.forEach(s => {
    (s.exercises || []).forEach(ex => {
      const kgs = (ex.sets || []).map(st => parseFloat(st.kg) || 0).filter(k => k > 0);
      if (kgs.length === 0) return;
      const maxKg = Math.max(...kgs);
      if (!exerciseKgHistory[ex.name]) exerciseKgHistory[ex.name] = [];
      exerciseKgHistory[ex.name].push(maxKg);
    });
  });

  // Detect plateaus (same max kg in last 3+ consecutive appearances)
  const plateaus = [];
  Object.entries(exerciseKgHistory).forEach(([name, kgs]) => {
    if (kgs.length >= 3) {
      const last3 = kgs.slice(0, 3);
      if (last3.every(k => k === last3[0]) && last3[0] > 0) {
        plateaus.push({ name, kg: last3[0], sessions: last3.length });
      }
    }
  });

  // Low volume groups
  const lowVolGroups = [];
  if (pushVol > 0 && pullVol === 0) lowVolGroups.push('tracción');
  else if (pushPullRatio > 1.5 && pushVol > 3) lowVolGroups.push('espalda');
  if (legVol < 6 && monthSessions.length > 4 && weekSessions.length > 1) lowVolGroups.push('piernas');
  if (coreVol === 0 && weekSessions.length > 1) lowVolGroups.push('core');

  // Fatigue
  const totalSetsWeek = weekSessions.reduce((t, s) =>
    t + (s.exercises || []).reduce((ts, ex) => ts + (ex.sets?.length || 1), 0), 0);
  const avgSetsPerSess = weekSessions.length > 0 ? totalSetsWeek / weekSessions.length : 0;
  const fatigueLevel = (totalSetsWeek > 60 || avgSetsPerSess > 22) ? 'high'
    : totalSetsWeek > 30 ? 'moderate' : 'low';

  // Adherence
  const targetDays = profile?.dias || profile?.trainingDays || 3;
  const adherenceRate = weekSessions.length / targetDays;

  // Most trained exercise
  const exCount = {};
  log.slice(0, 12).forEach(s => {
    (s.exercises || []).forEach(ex => { exCount[ex.name] = (exCount[ex.name] || 0) + 1; });
  });
  const mostTrained = Object.entries(exCount).sort((a, b) => b[1] - a[1])[0]?.[0] || null;

  // Muscle priority volume gaps (how far priority muscles are from target)
  const musclePriorities = profile?.musclePriorities || [];
  const priorityGaps = [];
  const MUS_VOL_CHECK = { chest:pushVol, back:pullVol, legs:legVol };
  musclePriorities.forEach(m => {
    const vol = MUS_VOL_CHECK[m];
    if (vol !== undefined && vol < 8) priorityGaps.push({ muscle: m, vol, label: AP_MUS_MAP[m] || m });
  });

  // Injury risk: pushing shoulder-heavy volume with shoulder injury
  const injuries = profile?.injuries || [];
  const injuryRisks = [];
  if (injuries.includes('shoulder') && pushVol > pullVol * 1.2 && pushVol > 3) {
    injuryRisks.push({ muscle:'hombro', warn:`${pushVol} series de empuje vs ${pullVol} de tracción con limitación en hombro` });
  }

  // Read active Builder plan for Coach read-back
  const builderPlan = (() => {
    try { return JSON.parse(localStorage.getItem('atlas.builder.plan.v1') || 'null'); } catch { return null; }
  })();
  const activeRoutine = (() => {
    try { return JSON.parse(localStorage.getItem(AR_KEY) || 'null'); } catch { return null; }
  })();

  return {
    log, sessions,
    totalSessions: log.length,
    daysSinceLast,
    weekSessions: weekSessions.length,
    monthSessions: monthSessions.length,
    pushVol, pullVol, legVol, coreVol, pushPullRatio,
    weekMuscleVol, exerciseKgHistory,
    plateaus, lowVolGroups,
    fatigueLevel, totalSetsWeek, avgSetsPerSess,
    adherenceRate, targetDays,
    mostTrained,
    streak: sessions.streak || 0,
    completed: sessions.completed || 0,
    musclePriorities, priorityGaps, injuries, injuryRisks,
    builderPlan, activeRoutine,
    progressSummary: typeof peGetProgressSummary !== 'undefined' ? peGetProgressSummary(log) : null,
    sePanel: typeof AtlasScientificEngine !== 'undefined' ? AtlasScientificEngine.seGetScientificPanel(log, profile) : null,
    apePanel: typeof AtlasProgressionEngine !== 'undefined' ? AtlasProgressionEngine.getFullPanel(log, profile) : null,
  };
}

// ── Text robustness: accent-, case-, order- and typo-tolerant matching ─────────
// Mirrors the exercise-search philosophy (see services/exercises.jsx) so the Coach
// understands natural phrasing and misspellings, not just exact keywords. Kept
// local to this file so no global names collide with the babel-global scope.
function acNorm(s) {
  return (s == null ? '' : String(s))
    .toLowerCase()
    .normalize('NFD').replace(/[̀-ͯ]/g, '')       // strip accents
    .replace(/[¿?¡!.,;:()"'`]/g, ' ')                       // drop punctuation (keep / and -)
    .replace(/\s+/g, ' ').trim();
}
function acTokens(s) { return acNorm(s).split(' ').filter(Boolean); }

// Bounded Levenshtein — bails out as soon as it can't fit within `max`.
function acLev(a, b, max) {
  if (Math.abs(a.length - b.length) > max) return max + 1;
  const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
  for (let j = 1; j <= b.length; j++) {
    let prev = dp[0]; dp[0] = j; let best = dp[0];
    for (let i = 1; i <= a.length; i++) {
      const tmp = dp[i];
      dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
      prev = tmp; if (dp[i] < best) best = dp[i];
    }
    if (best > max) return max + 1;
  }
  return dp[a.length];
}
// Does the message (already tokenized + normalized-joined) contain `kw`?
// Multi-word kw → substring match on the joined text. Single word → exact,
// typo-tolerant (edit distance scaled by length), or contained-in-a-longer-token.
function acHasKw(tokens, joined, kw) {
  const k = acNorm(kw);
  if (!k) return false;
  if (k.includes(' ')) return joined.includes(k);
  const max = k.length >= 8 ? 2 : k.length >= 5 ? 1 : 0;
  return tokens.some(t =>
    t === k ||
    (max > 0 && acLev(t, k, max) <= max) ||
    (k.length >= 4 && t.length > k.length && t.includes(k))
  );
}
function acHasAny(tokens, joined, list) { return list.some(kw => acHasKw(tokens, joined, kw)); }

// Ordered keyword → intent table (typo-tolerant recall layer). Order matters:
// specific concerns (injury, plateau, recovery) are checked before the broad
// "routine" bucket so "me duele el ombro" never becomes a routine request.
const AC_INTENT_KW = [
  { intent:'injury',         kws:['duele','dolor','molestia','molesta','lesion','lesionado','lastimado','pinchazo','tendinitis'] },
  { intent:'plateau',        kws:['estancado','estancada','plateau','atascado','atascada','mismo peso','no subo','no progreso','no avanzo','sin avanzar'] },
  { intent:'exercise-doubt', kws:['tecnica','ejecucion','postura','agarre','como se hace','como se ejecuta','forma correcta'] },
  { intent:'recovery',       kws:['recuperacion','recuperar','descanso','descansar','fatiga','fatigado','sobreentrenamiento','deload','cansado','agotado','quemado'] },
  { intent:'volume-science', kws:['mrv','mav','mev','zona de volumen'] },
  { intent:'programming',    kws:['series','volumen','frecuencia','fallo','periodizacion','cuantas series','repeticiones'] },
  { intent:'session-analysis', kws:['ultima sesion','ultimo entreno','como fue','que tal fue','como me fue'] },
  { intent:'analysis',       kws:['analiza','analisis','revisa','historial','como voy','como lo llevo'] },
  { intent:'progress-check', kws:['progreso','progresando','evolucion','mejorado','mejorando','resultados','adherencia','constancia','racha'] },
  { intent:'routine',        kws:['rutina','entrenamiento','entrenar','entreno','plan','planificar','ejercicios','split','push','pull','legs','piernas','pecho','espalda','hombros','brazos','biceps','triceps','gluteos','fullbody'] },
  { intent:'profile',        kws:['perfil','objetivo','nivel'] },
  { intent:'greeting',       kws:['hola','buenas','saludos','holaa'] },
];
function acKeywordIntent(tokens, joined) {
  for (const row of AC_INTENT_KW) {
    if (acHasAny(tokens, joined, row.kws)) return row.intent;
  }
  return null;
}

// ── Response-variant rotation — "no disco rayado" ──────────────────────────────
// Cycles through phrasings per key so a repeated intent doesn't return the same
// identical block. The cursor persists so rotation survives reloads.
const AC_VARIANT_KEY = 'atlas.coach.variants.v1';
function acVariant(key, variants) {
  if (!variants || !variants.length) return '';
  let store = {};
  try { store = JSON.parse(localStorage.getItem(AC_VARIANT_KEY) || '{}') || {}; } catch {}
  const i = (store[key] || 0) % variants.length;
  store[key] = (i + 1) % variants.length;
  try { localStorage.setItem(AC_VARIANT_KEY, JSON.stringify(store)); } catch {}
  return variants[i];
}

// Movement-pattern technique cues for scripted exercise-doubt answers.
const AC_PATTERN_CUE = {
  'empuje-horizontal':      'retrae las escápulas, baja controlando 2–3 s y empuja sin bloquear el codo de golpe.',
  'empuje-vertical':        'core firme y glúteos apretados para no arquear la lumbar; recorrido completo por encima de la cabeza.',
  'traccion-horizontal':    'lleva los codos hacia atrás y aprieta la espalda al final; no tires solo con los brazos.',
  'traccion-vertical':      'inicia el tirón bajando los hombros; lleva la barra al pecho, nunca detrás del cuello.',
  'sentadilla':             'rodillas alineadas con las puntas de los pies, pecho arriba y baja al menos hasta paralelo.',
  'bisagra':                'bisagra de cadera con espalda neutra y la barra pegada al cuerpo durante todo el recorrido.',
  'aislamiento-pantorrilla':'rango completo: estira abajo del todo y contrae arriba, sin rebotes.',
};

// ── Intent + param extraction ─────────────────────────────────────────────────
function acDetectIntent(text) {
  const t   = acNorm(text);      // accent/punctuation-normalized (regexes below stay valid)
  const tok = t.split(' ').filter(Boolean);
  // Fuera de alcance — se comprueban primero para que el Coach sea honesto en
  // vez de improvisar una rutina a partir de palabras sueltas.
  if (/dieta|nutric|aliment|macros|calor[íi]as|qu[eé] (como|comer|debo comer)|plan de comida|men[uú] semanal|d[eé]ficit cal[oó]rico|super[aá]vit cal[oó]rico|cu[aá]nta prote[íi]na/.test(t)) return 'nutrition';
  if (/seguimiento personal|que me sigas|me sigas|acompa[nñ]amiento|entrenador personal|coach personal|entrenador en persona|seguimiento semanal|que alguien me siga|plan personalizado a medida/.test(t)) return 'personal-coaching';
  if (/[uú]ltima sesi[oó]n|analiza.*sesi[oó]n|c[oó]mo fue mi (entrenamiento|sesi[oó]n)|mi entrenamiento de (hoy|ayer)|qu[eé] tal (fue|estuvo).*(entrena|sesi[oó]n)/.test(t)) return 'session-analysis';
  if (/hazme|g[eé]nera|crea|dame.*rutina|rutina.*para|plan.*para|d[íi]a de|quiero.*entrena|qu[eé] hago hoy/.test(t)) return 'routine';
  if (/quiero.*rutina|necesito.*rutina|una rutina|rutina de |d[aá]me.*rutina|h[aá]zme.*rutina/.test(t)) return 'routine';
  if (/quiero.*trabajar|mejorar.*pecho|mejorar.*espalda|mejorar.*pierna|foco en/.test(t)) return 'routine';
  if (/analiza|an[aá]lisis|c[oó]mo.*voy|qu[eé] tal.*entrena|revisar?|mi historial|mi progreso/.test(t)) return 'analysis';
  if (/me duele|me molesta|tengo.*dolor|lesi[oó]n|lastim|dolor en|molestia en/.test(t)) return 'injury';
  if (/t[eé]cnica|ejecuci[oó]n|forma correcta|c[oó]mo se (hace|ejecuta|realiza)|agarre correcto|postura correcta|lo (hago|estoy haciendo) bien|c[oó]mo hacer bien/.test(t)) return 'exercise-doubt';
  if (/estancado|plateau|no progres|no avanzo|mismo peso|sin cambios/.test(t)) return 'plateau';
  if (/mi volumen de|volumen.*m[uú]sculo|cu[aá]ntas series.*tengo|mav|mrv|mev|zona de volumen|en qu[eé] zona|series.*m[uú]sculo|mi.*frecuencia real|frecuencia.*m[uú]sculo/.test(t)) return 'volume-science';
  if (/cu[aá]ntas series|volumen|frecuencia|mejor.*frecuencia|fallo muscular|rir|rpe/.test(t)) return 'programming';
  if (/recupera|descans|hrv|fatiga|sobreentren|puedo.*entrenar|debo descansar|deload/.test(t)) return 'recovery';
  if (/revisa.*rutina|cómo.*está.*mi.*plan|analiza.*mi.*rutina|mi.*rutina.*actual|qué.*tiene.*mi.*plan|mi.*plan.*actual/.test(t)) return 'builder-review';
  if (/estoy progresando|cu[aá]nto.*mejor|mi evoluci[oó]n|he mejorado|cu[aá]nto he.*subid|qu[eé].*progres|mis resultados|mi historial de carga/.test(t)) return 'progress-check';
  if (/adherencia|cu[aá]nto.*entreno|cumpl|consistencia|racha|sesiones.*mes|mi.*progreso.*global|atlas.*score/.test(t)) return 'progress-check';
  if (/perfil|mi.*objetivo|mi.*nivel|cambiar.*objetivo|actualiz.*perfil/.test(t)) return 'profile';
  if (/^(hola|hey|buenas|buenos|qu[eé] hay|ola)/.test(t)) return 'greeting';
  // A bare split / muscle-group answer is itself a routine request — short replies
  // like "upper lower", "ppl", "full body", "pull", "pecho"… (anchored so only a
  // whole-message split token triggers, never a longer sentence).
  if (/^[\s\-•*]*(upper\s*[\/\-]?\s*lower|lower\s*[\/\-]?\s*upper|superior\s*[\/\-]?\s*inferior|ppl|push\s*[\/\-]?\s*pull\s*[\/\-]?\s*legs?|full\s*-?\s*body|cuerpo entero|todo el cuerpo|push|pull|legs?|piernas?|empuje|tracci[oó]n|pecho|espalda|hombros?|brazos?|b[íi]ceps?|tr[íi]ceps?|gl[uú]teos?)[\s.!¡·]*$/.test(t.trim())) return 'routine';
  // Objetivo / nivel / disponibilidad declarados → petición de rutina. Sobre el
  // texto ya normalizado (sin tildes): p. ej. "soy principiante y quiero ganar
  // musculo, puedo ir 3 dias".
  if (/ganar m[uú]sculo|ganar musculo|ganar fuerza|perder grasa|perder peso|ponerme en forma|tonificar|quiero (entrenar|empezar|ponerme|ganar|perder|mejorar|hacer ejercicio|hacer deporte)|soy (principiante|novato|intermedio|avanzado)|empiezo a entrenar|nunca he entrenado|puedo (ir|entrenar) \d|entreno \d|\d (d[ií]as?|veces) (a|por) (la )?semana|\d d[ií]as? (a la |por )?semana/.test(t)) return 'routine';
  // Typo-/variation-tolerant recall layer — catches loose phrasing and common
  // misspellings the patterns above miss (e.g. "rutna", "entreno", "recuperacon").
  const kw = acKeywordIntent(tok, t);
  if (kw) return kw;
  return 'general';
}

function acExtractParams(text) {
  const t = text.toLowerCase();
  const p = { raw: text };
  if (/mancuerna/.test(t)) p.equipment = 'mancuernas';
  else if (/en casa|sin.*gym|bodyweight|peso corporal/.test(t)) p.equipment = 'casa';
  const timeM = t.match(/(\d+)\s*min/);
  if (timeM) p.tiempo = parseInt(timeM[1]);
  if (/pecho|push|empuje/.test(t)) p.target = 'push';
  else if (/espalda|pull|tracci[oó]n|dorsal/.test(t)) p.target = 'pull';
  else if (/pierna|piernas|legs?|cuadricep|sentadilla|gl[uú]teo/.test(t)) p.target = 'legs';
  else if (/brazo|b[íi]cep|tr[íi]cep/.test(t)) p.target = 'arms';
  else if (/hombro/.test(t)) p.target = 'shoulders';
  else if (/full|completo|todo/.test(t)) p.target = 'full';
  if (/ppl|push.*pull.*leg/.test(t)) p.split = 'ppl';
  else if (/upper.*lower|superior.*inferior/.test(t)) p.split = 'upper_lower';
  else if (/full.?body|cuerpo entero/.test(t)) p.split = 'fullbody';
  if (/fuerza|strength/.test(t)) p.goal = 'fuerza';
  else if (/hipertrofia|ganar m[uú]sculo|ganar musculo/.test(t)) p.goal = 'hipertrofia';
  else if (/recomp|perder|definir/.test(t)) p.goal = 'recomp';
  // Nivel declarado en el mensaje ("soy principiante", "empiezo"…)
  if (/principiante|novato|empiezo|empezar a entrenar|nunca he entrenado|primera vez/.test(t)) p.nivel = 'principiante';
  else if (/avanzad|experto/.test(t)) p.nivel = 'avanzado';
  else if (/intermedio/.test(t)) p.nivel = 'intermedio';
  const diasM = t.match(/(\d)\s*d[íi]a/);
  if (diasM) p.dias = parseInt(diasM[1]);
  // Explicit exercise count: "rutina de 7 ejercicios", "algo de 5"
  const countM = t.match(/(\d+)\s*ejercicio/);
  if (countM) p.count = Math.min(12, Math.max(3, parseInt(countM[1])));
  // Short/quick session shorthand
  else if (/algo corto|sesi[oó]n corta|r[aá]pido|r[aá]pida|express/.test(t)) p.count = 4;
  return p;
}

// ── Detailed routine builder ──────────────────────────────────────────────────
function acBuildDetailedRoutine(splitKey, goal, level, tiempo, allExs, groupCaps = {}, avoidKeywords = [], musclePriorities = [], targetCount = null, log = []) {
  const byGroup = {};
  allExs.forEach(ex => {
    if (avoidKeywords.length) {
      const nm = (ex.name || '').toLowerCase();
      if (avoidKeywords.some(kw => kw && nm.includes(kw))) return;
    }
    const g = acExGroup(ex);
    if (!byGroup[g]) byGroup[g] = [];
    byGroup[g].push(ex);
  });

  // Plateau-aware ordering: push stagnant exercises to the end of each group
  // so non-stagnant alternatives are preferred when available.
  const stagnantNames = new Set(
    log.length >= 3 && typeof peGetStagnantExercises !== 'undefined'
      ? peGetStagnantExercises(log, 3).map(e => e.name)
      : []
  );
  if (stagnantNames.size > 0) {
    Object.keys(byGroup).forEach(g => {
      byGroup[g].sort((a, b) =>
        (stagnantNames.has(a.name) ? 1 : 0) - (stagnantNames.has(b.name) ? 1 : 0)
      );
    });
  }

  const scheme = {
    hipertrofia:             { sets: 3, setsComp: 4, reps: '8-12',  rir: 2, rest: '90-120s' },
    'pérdida de grasa':      { sets: 3, setsComp: 3, reps: '12-15', rir: 2, rest: '60s'     },
    'recomposición corporal':{ sets: 3, setsComp: 4, reps: '10-14', rir: 2, rest: '75s'     },
    fuerza:                  { sets: 4, setsComp: 5, reps: '3-6',   rir: 1, rest: '3-5 min' },
    rendimiento:             { sets: 4, setsComp: 5, reps: '4-8',   rir: 1, rest: '2-3 min' },
    'rendimiento deportivo': { sets: 4, setsComp: 5, reps: '4-8',   rir: 1, rest: '2-3 min' },
  }[goal] || { sets: 3, setsComp: 4, reps: '8-12', rir: 2, rest: '90s' };

  // Pick `n` exercises from a group starting at `offset`, marking first as compound
  function pick(group, n, isCompound, offset = 0) {
    const cap = groupCaps[group];
    const effectiveN = cap !== undefined ? Math.min(n, cap) : n;
    if (effectiveN === 0) return [];
    return (byGroup[group] || []).slice(offset, offset + effectiveN).map((ex, i) => {
      const setsN   = isCompound && i === 0 ? scheme.setsComp : scheme.sets;
      const isComp  = isCompound && i === 0;
      const isStag  = stagnantNames.has(ex.name);

      // ── History preloading & progression hint ─────────────────────────────
      let progressionHint = null;
      let preloadKg       = '';
      if (log.length > 0 && typeof peGetProgressionHint !== 'undefined') {
        const exIsLower = /sentadilla|bisagra|pantorrilla/.test(ex.pattern || '');
        const ph = peGetProgressionHint(log, ex.name, { isCompound: !!ex.compound, isLower: exIsLower, repsRange: scheme.reps });
        if (ph) {
          if (isStag) {
            // Plateau: show last load but no increase suggestion
            const fmtKg = v => (v % 1 === 0 ? String(v) : v.toFixed(1));
            progressionHint = `Última vez: ${ph.preloadReps} reps · ${fmtKg(ph.preloadKg)} kg · prueba variante`;
            preloadKg = ph.preloadKg;
          } else {
            progressionHint = ph.hint;
            preloadKg = ph.suggestedKg != null ? ph.suggestedKg : ph.preloadKg;
          }
        }
      }

      return {
        id: ex.id,
        name: ex.name,
        muscles: { primary: (ex.muscles?.primary || []).slice(0, 2), secondary: [] },
        pattern: ex.pattern,
        sets: Array.from({ length: setsN }, () => ({
          kg:   preloadKg ? String(preloadKg) : '',
          reps: scheme.reps.split('-')[0],
        })),
        setsCount:       setsN,
        repsRange:       scheme.reps,
        rir:             i === 0 ? scheme.rir : scheme.rir + 1,
        rest:            isCompound && i === 0 ? scheme.rest : '60-90s',
        progressionHint: progressionHint || undefined,
        plateauAlert:    isStag || undefined,
      };
    });
  }

  // ── Volume scaling ────────────────────────────────────────────────────────────
  // lvlScale: 0 = principiante, 1 = intermedio, 2 = avanzado
  const lvlScale = { principiante: 0, intermedio: 1, avanzado: 2 }[level] ?? 1;

  // Map muscle groups to musclePriorities keys (from atlas profile)
  const G_TO_PRIO = { pecho:'chest', espalda:'back', piernas:'legs', gluteos:'glutes', hombro:'shoulders', biceps:'arms', triceps:'arms' };
  const priBonus = (g) => musclePriorities.includes(G_TO_PRIO[g]) ? 1 : 0;

  // Max exercises per session from available time (duration = 5 + totalSets*2.8, avg ~3.5 sets/ex → ~9.8 min/ex)
  const timeMaxEx = tiempo ? Math.max(3, Math.floor((tiempo - 5) / 9.8)) : 99;
  // Session ceiling: level cap OR user-requested count, whichever is more restrictive
  const sessionCeil = targetCount
    ? Math.min(targetCount, timeMaxEx)
    : Math.min({ principiante: 5, intermedio: 7, avanzado: 9 }[level] || 7, timeMaxEx);
  // Session floor: minimum we aim to reach (level-dependent)
  const sessionFloor = { principiante: 4, intermedio: 5, avanzado: 6 }[level] || 5;

  // Build one session from a spec array: [{ group, base, ext, compound }]
  // base = count at principiante, ext = added at intermedio (+1 more at avanzado)
  function buildSession(name, spec) {
    const usage = {}; // track how many exercises taken per group
    let exs = [];

    // Pass 1: fill from spec with level scaling + priority bonuses
    spec.forEach(({ group, base, ext, compound }) => {
      const desired = base + (lvlScale >= 1 ? ext : 0) + (lvlScale >= 2 ? 1 : 0) + priBonus(group);
      const actual  = Math.min(desired, groupCaps[group] ?? 99, (byGroup[group] || []).length);
      usage[group]  = actual;
      exs.push(...pick(group, actual, compound, 0));
    });

    // Pass 2: compensate if below floor (group exhausted) — pull extras from most-available groups
    if (exs.length < sessionFloor && exs.length < sessionCeil) {
      // Try each group in spec order (isolation groups first, so we add variety)
      const specOrdered = [...spec].reverse();
      for (const { group, compound } of specOrdered) {
        if (exs.length >= sessionFloor) break;
        const offset = usage[group] || 0;
        const available = (byGroup[group] || []).slice(offset);
        if (available.length > 0) {
          const extra = pick(group, 1, false, offset);
          if (extra.length) { exs.push(...extra); usage[group] = offset + 1; }
        }
      }
    }

    // Trim to ceiling
    if (exs.length > sessionCeil) exs = exs.slice(0, sessionCeil);

    return { name, exs };
  }

  const configs = {
    // Intermediate target: 3 pecho (1 comp + 2 isol) + 2 hombro + 2 tríceps = 7
    push: () => [buildSession('Push — Empuje', [
      { group:'pecho',   base:2, ext:1, compound:true  },  // beg:2 / int:3 / adv:4
      { group:'hombro',  base:1, ext:1, compound:false },  // beg:1 / int:2 / adv:3
      { group:'triceps', base:1, ext:1, compound:false },  // beg:1 / int:2 / adv:3
    ])],
    // Intermediate target: 3 espalda + 1 hombro posterior + 2 bíceps = 6
    pull: () => [buildSession('Pull — Tracción', [
      { group:'espalda', base:2, ext:1, compound:true  },  // beg:2 / int:3 / adv:4
      { group:'hombro',  base:0, ext:1, compound:false },  // beg:0 / int:1 / adv:2 (delt. posterior)
      { group:'biceps',  base:1, ext:1, compound:false },  // beg:1 / int:2 / adv:3
    ])],
    // Intermediate target: 3 piernas + 2 glúteos + 1 core = 6
    legs: () => [buildSession('Legs — Piernas', [
      { group:'piernas', base:2, ext:1, compound:true  },  // beg:2 / int:3 / adv:4
      { group:'gluteos', base:1, ext:1, compound:true  },  // beg:1 / int:2 / adv:2
      { group:'core',    base:1, ext:0, compound:false },  // beg:1 / int:1 / adv:2
    ])],
    arms: () => [buildSession('Brazos', [
      { group:'biceps',  base:2, ext:1, compound:false },
      { group:'triceps', base:2, ext:1, compound:false },
    ])],
    shoulders: () => [buildSession('Hombros', [
      { group:'hombro',  base:2, ext:1, compound:false },
      { group:'triceps', base:1, ext:1, compound:false },
    ])],
    fullbody: () => [buildSession('Full Body', [
      { group:'piernas', base:1, ext:1, compound:true  },
      { group:'pecho',   base:1, ext:0, compound:true  },
      { group:'espalda', base:1, ext:0, compound:true  },
      { group:'hombro',  base:1, ext:0, compound:false },
      { group:'core',    base:1, ext:0, compound:false },
    ])],
    ppl: () => [
      buildSession('Push — Empuje', [
        { group:'pecho',   base:2, ext:1, compound:true  },
        { group:'hombro',  base:1, ext:1, compound:false },
        { group:'triceps', base:1, ext:1, compound:false },
      ]),
      buildSession('Pull — Tracción', [
        { group:'espalda', base:2, ext:1, compound:true  },
        { group:'hombro',  base:0, ext:1, compound:false },
        { group:'biceps',  base:1, ext:1, compound:false },
      ]),
      buildSession('Legs — Piernas', [
        { group:'piernas', base:2, ext:1, compound:true  },
        { group:'gluteos', base:1, ext:1, compound:true  },
        { group:'core',    base:1, ext:0, compound:false },
      ]),
    ],
    upper_lower: () => [
      buildSession('Upper — Tren superior', [
        { group:'pecho',   base:1, ext:1, compound:true  },
        { group:'espalda', base:1, ext:1, compound:true  },
        { group:'hombro',  base:1, ext:0, compound:false },
        { group:'biceps',  base:0, ext:1, compound:false },
        { group:'triceps', base:0, ext:1, compound:false },
      ]),
      buildSession('Lower — Tren inferior', [
        { group:'piernas', base:2, ext:1, compound:true  },
        { group:'gluteos', base:1, ext:1, compound:true  },
        { group:'core',    base:1, ext:0, compound:false },
      ]),
    ],
  };

  const builder = configs[splitKey] || configs.fullbody;
  return builder().map(s => {
    const exercises = s.exs.filter(e => e && e.id);
    const totalSets = exercises.reduce((t, e) => t + (e.setsCount ?? (Array.isArray(e.sets) ? e.sets.length : e.sets) ?? 3), 0);
    return {
      name: s.name,
      exercises,
      totalSets,
      duration: Math.round(5 + totalSets * 2.8),
      muscles: [...new Set(exercises.flatMap(e => e.muscles?.primary?.slice(0, 1) || []))],
    };
  }).filter(s => s.exercises.length > 0);
}

// ── Plantillas como única fuente de verdad (Coach ⇄ Plantillas) ────────────────
// El Coach NO inventa planes con una lógica propia: parte de las plantillas
// predeterminadas (presetRoutines.js) y su selector de nivel. Recomienda la
// plantilla según nivel + días y la renderiza al nivel adecuado (principiante =
// guiado, sin barra libre por defecto, RIR conservador 3–4).
const AC_PRESET_FULLBODY   = 'preset-fullbody-principiante';
const AC_PRESET_UPPERLOWER = 'preset-upperlower-intermedio';
const AC_PRESET_PPL        = 'preset-ppl-avanzado';

function acPresetById(id) {
  try { return (window.PRESET_ROUTINES || []).find(p => p.id === id) || null; }
  catch { return null; }
}

// Recomendación por nivel + días → una de las 3 plantillas de split.
function acRecommendPreset(level, days) {
  const d = Math.max(1, Math.min(7, Number(days) || 3));
  if (level === 'principiante') {
    if (d >= 4) return {
      id: AC_PRESET_UPPERLOWER, renderLevel: 'principiante',
      reason: `Aunque quieras ${d} días, como principiante progresas más con un Upper/Lower de 4 días a intensidad controlada (RIR 3–4): primero técnica y recuperación, no acumular días.`,
      note: 'Lo he ajustado a 4 días (Upper/Lower) en nivel principiante. Cuando domines los patrones subimos volumen y frecuencia.',
    };
    return {
      id: AC_PRESET_FULLBODY, renderLevel: 'principiante',
      reason: `Con ${d} días, el cuerpo completo te permite trabajar cada músculo 2–3 veces por semana — lo ideal para empezar.`,
    };
  }
  // Intermedio / avanzado
  if (d >= 5) return {
    id: AC_PRESET_PPL, renderLevel: level === 'avanzado' ? 'avanzado' : 'intermedio',
    reason: `Con ${d} días, un Push/Pull/Legs reparte el volumen en 6 sesiones y da alta frecuencia por grupo muscular.`,
    note: d === 5 ? 'Con 5 días puedes rotar las 6 sesiones en 8 días o repetir el día que más te interese.' : undefined,
  };
  if (d === 4) return {
    id: AC_PRESET_UPPERLOWER, renderLevel: level,
    reason: 'Con 4 días, Upper/Lower trabaja cada músculo 2 veces por semana con buena recuperación.',
  };
  return {
    id: AC_PRESET_FULLBODY, renderLevel: level,
    reason: `Con ${d} días, el cuerpo completo te permite estimular cada músculo 2–3 veces por semana.`,
  };
}

// Petición de un grupo concreto → sesión de la plantilla correspondiente.
function acPresetForTarget(target) {
  switch (target) {
    case 'legs':      return { id: 'preset-pierna-hipertrofia', label: 'Pierna' };
    case 'arms':      return { id: 'preset-brazo-hipertrofia',  label: 'Brazo' };
    case 'shoulders': return { id: 'preset-brazo-hipertrofia',  label: 'Hombro y brazo' };
    case 'push':      return { id: AC_PRESET_PPL, dayNames: ['Push A · Pecho'],          label: 'Push (empuje)' };
    case 'pull':      return { id: AC_PRESET_PPL, dayNames: ['Pull A · Espalda gruesa'], label: 'Pull (tracción)' };
    default:          return null;
  }
}

// Decide qué plantilla servir a partir de la petición (split explícito, grupo, o
// nivel+días). Principiante nunca recibe PPL: se reconduce a Full Body / Upper-Lower.
function acPlanRecommendation(params, level, days) {
  const lvl = level || 'intermedio';
  const raw = (params.raw || '').toLowerCase();
  const wholePlan = /semana|plan|programa|rutina completa|completo|todo el cuerpo/.test(raw);

  // Grupo concreto (sin contexto de plan semanal).
  if (params.target && !params.split && !params.dias && !wholePlan) {
    const t = acPresetForTarget(params.target);
    if (t) return {
      id: t.id, renderLevel: lvl, dayFilter: t.dayNames,
      reason: `Sesión basada en la plantilla de ${t.label} de Atlas, adaptada a tu nivel (${lvl}).`,
    };
  }
  // Split pedido explícitamente.
  if (params.split === 'ppl') {
    if (lvl === 'principiante') return acRecommendPreset('principiante', days);
    return { id: AC_PRESET_PPL, renderLevel: lvl === 'avanzado' ? 'avanzado' : 'intermedio',
      reason: 'Push/Pull/Legs: 6 sesiones y alta frecuencia por grupo muscular.' };
  }
  if (params.split === 'upper_lower') return { id: AC_PRESET_UPPERLOWER, renderLevel: lvl,
    reason: 'Upper/Lower: cada músculo 2 veces por semana con buena recuperación.' };
  if (params.split === 'fullbody') return { id: AC_PRESET_FULLBODY, renderLevel: lvl,
    reason: 'Full Body: cada músculo 2–3 veces por semana en una sola sesión.' };

  // Por nivel + días.
  return acRecommendPreset(lvl, days);
}

// Convierte una plantilla (vía PresetTemplates) a las sesiones que renderiza el
// Coach, respetando el RIR por nivel (principiante = RIR ≥3, lejos del fallo) y
// precargando las cargas del historial real (misma lógica que Builder/plantillas).
function acSessionsFromPreset(preset, renderLevel, log, dayFilter) {
  if (!preset || typeof window.PresetTemplates === 'undefined') return [];
  let days = PresetTemplates.days(preset) || [];
  if (Array.isArray(dayFilter) && dayFilter.length) {
    const want = new Set(dayFilter);
    const sel = days.filter(d => want.has(d.name));
    if (sel.length) days = sel;
  }
  const beginner = renderLevel === 'principiante';
  return days.map(day => {
    const exercises = (day.exercises || []).map(pe => {
      const sh = PresetTemplates.toShared(pe, renderLevel);
      const isometric = /isom|al fallo/i.test(sh.repsRange || '');
      const prim = Array.isArray(sh.muscles?.primary) ? sh.muscles.primary
                 : (Array.isArray(sh.muscles) ? sh.muscles : []);
      // RIR por nivel: el principiante entrena conservador (RIR ≥3), nunca al fallo.
      let rir = sh.rir;
      if (beginner && !isometric) rir = Math.max(Number(sh.rir) || 3, 3);
      // Precarga desde el historial real (si existe).
      let sets = sh.sets, progressionHint;
      if ((log || []).length && typeof peGetProgressionHint !== 'undefined') {
        const pat = sh.pattern || '';
        const ph = peGetProgressionHint(log, sh.name, {
          isCompound: /empuje|traccion|sentadilla|bisagra/.test(pat),
          isLower: /sentadilla|bisagra|pantorrilla/.test(pat),
          repsRange: sh.repsRange,
        });
        if (ph) {
          const kg = ph.suggestedKg != null ? ph.suggestedKg : ph.preloadKg;
          if (kg != null) sets = sh.sets.map(s => ({ ...s, kg: String(kg) }));
          progressionHint = ph.hint || undefined;
        }
      }
      return {
        id: sh.id,
        name: sh.name,
        muscles: { primary: prim.slice(0, 2), secondary: [] },
        pattern: sh.pattern,
        sets,
        setsCount: sh.setsCount,
        repsRange: isometric ? (/fallo/i.test(sh.repsRange) ? 'Al fallo' : '20–40 s') : sh.repsRange,
        rir: isometric ? '' : rir,
        rest: sh.rest || '90s',
        progressionHint,
      };
    });
    const totalSets = exercises.reduce((t, e) => t + (e.setsCount ?? (Array.isArray(e.sets) ? e.sets.length : 3)), 0);
    return {
      name: day.name,
      exercises,
      totalSets,
      duration: Math.round(5 + totalSets * 2.8),
      muscles: [...new Set(exercises.flatMap(e => e.muscles?.primary?.slice(0, 1) || []))],
    };
  }).filter(s => s.exercises.length > 0);
}

// ── Response generators ───────────────────────────────────────────────────────
function acResponseGreeting(ctx, profile, memory) {
  const injuries = profile?.injuries || [];
  const musclePriorities = profile?.musclePriorities || [];

  if (ctx.totalSessions === 0) {
    if (!profile) {
      return { type:'text', text:`Hola. Soy Atlas Coach. Puedo generarte una rutina, resolver dudas de un ejercicio o analizar tu progreso. ¿Por dónde empezamos?` };
    }
    const parts = [`Hola. He cargado tu perfil — objetivo: ${profile.objetivo}, nivel ${profile.nivel}, ${profile.dias} días/sem.`];
    if (injuries.length) parts.push(`Tengo en cuenta tus limitaciones en ${injuries.map(i => i.replace(/_/g,' ')).join(' y ')}.`);
    if (musclePriorities.length) parts.push(`Grupos prioritarios: ${musclePriorities.map(m => AP_MUS_MAP[m] || m).join(', ')}.`);
    parts.push(`Sin sesiones registradas aún. ¿Generamos tu primer plan?`);
    return { type:'text', text: parts.join(' ') };
  }

  const parts = [];
  if (ctx.daysSinceLast === 0) parts.push('Entrenaste hoy.');
  else if (ctx.daysSinceLast === 1) parts.push('Entrenaste ayer.');
  else if (ctx.daysSinceLast < 99) parts.push(`Llevas ${ctx.daysSinceLast} días sin entrenar.`);
  if (ctx.streak >= 3) parts.push(`${ctx.streak} días de racha.`);
  if (ctx.injuryRisks?.length > 0) {
    parts.push(`⚠ Atención: ${ctx.injuryRisks[0].warn}.`);
  } else if (ctx.priorityGaps?.length > 0) {
    parts.push(`${ctx.priorityGaps[0].label} — tu músculo prioritario — tiene poco volumen esta semana (${ctx.priorityGaps[0].vol} series).`);
  } else if (ctx.pushPullRatio > 1.5 && ctx.pushVol > 3) {
    parts.push(`Más empuje que tracción esta semana (${ctx.pushVol} vs ${ctx.pullVol} series).`);
  } else if (ctx.plateaus.length > 0) {
    parts.push(`Posible plateau en ${ctx.plateaus[0].name}.`);
  } else if (ctx.lowVolGroups.length > 0) {
    parts.push(`${ctx.lowVolGroups[0]} con poco volumen esta semana.`);
  }
  if (memory?.lesiones?.length > 0) parts.push(`Recuerdo que tienes molestias en ${memory.lesiones.slice(0,2).join(' y ')}.`);
  parts.push(acVariant('greet-close', ['¿En qué te ayudo?', '¿Por dónde quieres empezar?', 'Dime en qué te echo una mano.']));
  return { type:'text', text: parts.join(' ') };
}

function acResponseRoutine(params, ctx, profile, memory, allExs) {
  // If no muscle group / split specified and the request is ambiguous, ask for clarification
  if (!params.target && !params.split && !params.dias) {
    const raw = (params.raw || '').toLowerCase();
    const hasWeekContext = /semana|plan|programa|d[íi]as|completo|todo|personaliz|a medida/.test(raw);
    if (!hasWeekContext) {
      return {
        type: 'text',
        // awaitSplit: the next user message should be read as the answer to this
        // question (see acGenerateSmartResponse conversational memory).
        awaitSplit: true,
        text: acVariant('routine-ask', ['¿Qué tipo de rutina quieres?', '¿Qué te preparo?', 'Dime qué buscas y lo armo al instante.']) + '\n\n• **Push** — pecho, hombros y tríceps\n• **Pull** — espalda y bíceps\n• **Legs** — cuádriceps, isquiotibiales y glúteos\n• **Full Body** — todo el cuerpo en una sesión\n• **PPL completo** — plan Push/Pull/Legs de toda la semana\n• **Upper / Lower** — plan de semana dividido en superior/inferior\n\nDime cuál prefieres (o cuántos días entrenas por semana) y lo armo al instante.',
      };
    }
  }

  const goal           = params.goal   || profile?.objetivo        || 'hipertrofia';
  const tiempo         = params.tiempo || profile?.tiempo          || 60;
  const level          = params.nivel  || profile?.nivel           || 'intermedio';
  const injuries       = profile?.injuries                         || [];
  const musclePriorities = profile?.musclePriorities              || [];
  const avoidExercises = profile?.avoidExercises                  || null;
  const days           = params.dias   || profile?.dias || ctx.targetDays || 3;

  // ── Plantillas predeterminadas como ÚNICA fuente de verdad ───────────────────
  // El Coach recomienda y sirve una plantilla real (presetRoutines.js) según
  // nivel + días, renderizada al nivel adecuado por su selector. Solo si no hay
  // plantilla aplicable (o PresetTemplates no está cargado) cae al generador
  // propio de abajo, para no quedarse nunca sin respuesta.
  const rec = acPlanRecommendation(params, level, days);
  const presetPlan = acPresetById(rec.id);
  const presetSessions = presetPlan ? acSessionsFromPreset(presetPlan, rec.renderLevel, ctx.log || [], rec.dayFilter) : [];
  if (presetPlan && presetSessions.length) {
    const raw = (params.raw || '').toLowerCase();
    const personalized = /personaliz|a medida|hecho a medida|exclusiv|solo para m[ií]/.test(raw);

    const introLines = [];
    // (3) "Personalizado" → aviso + derivación a un profesional titulado.
    if (personalized) {
      introLines.push('Te propongo un plan según lo que me has contado, basado en las rutinas de Atlas. Si quieres un plan hecho a medida por un profesional, lo recomendable es acudir a un graduado en Ciencias de la Actividad Física y del Deporte.');
    }
    const single = presetSessions.length === 1;
    const title = single ? presetSessions[0].name : presetPlan.name;
    introLines.push(`${acVariant('routine-intro', ['Aquí tienes tu plan', 'Listo — te he preparado este plan', 'He preparado este plan'])}: **${title}** (nivel ${rec.renderLevel}).`);
    // (1) Una línea con el porqué + enlace al artículo de frecuencia (abajo).
    if (rec.reason) introLines.push(`📅 ${rec.reason}`);
    if (rec.note)   introLines.push(rec.note);
    if (rec.renderLevel === 'principiante') {
      introLines.push('Cargas conservadoras y lejos del fallo (RIR 3–4) con variantes guiadas: primero dominas la técnica, la carga llega después.');
    }
    // Solo con historial real y un parón largo (≥14 días). Sin sesiones
    // registradas no se muestra (daysSinceLast = 99 por defecto para nuevos).
    if (ctx.totalSessions > 0 && ctx.daysSinceLast >= 14) introLines.push(`Llevas ${ctx.daysSinceLast} días sin entrenar — empieza al 70–80% de tus cargas habituales.`);
    if (avoidExercises) introLines.push(`Si algún ejercicio te molesta, cámbialo por su alternativa en el Builder (p. ej. lo que indicaste evitar: ${avoidExercises}).`);

    const routineId = `routine-${Date.now()}`;
    const atlasRoutine = {
      id:         routineId,
      name:       `${presetPlan.name} · ${rec.renderLevel}`,
      objective:  presetPlan.objective || goal,
      frequency:  days,
      split:      rec.id,
      sessions:   presetSessions,
      priorities: musclePriorities,
      createdAt:  Date.now(),
      source:     'coach',
      presetId:   presetPlan.id,
      level:      rec.renderLevel,
    };
    arSave(atlasRoutine);

    return {
      type: 'routine',
      text: introLines.join('\n\n'),
      sessions: presetSessions,
      builderPayload: presetSessions[0]?.exercises || [],
      routineId,
      routineName: atlasRoutine.name,
      relatedArticles: ['frecuencia-entrenamiento'],
    };
  }

  // Smart split selection
  let splitKey = params.split || null;
  if (!splitKey) {
    if      (params.target === 'push')      splitKey = 'push';
    else if (params.target === 'pull')      splitKey = 'pull';
    else if (params.target === 'legs')      splitKey = 'legs';
    else if (params.target === 'arms')      splitKey = 'arms';
    else if (params.target === 'shoulders') splitKey = 'shoulders';
    else {
      // Prefer split that benefits priority muscles
      const prioSplits = musclePriorities.map(m =>
        m === 'back' ? 'pull' : m === 'chest' ? 'push' : m === 'legs' || m === 'glutes' ? 'legs' : null
      ).filter(Boolean);
      splitKey = AP_DAYS_SPLIT[days] || (days >= 4 ? 'upper_lower' : days >= 3 ? 'ppl' : 'fullbody');
      // If user asked for a specific priority and it maps to a single day, honour it
      if (prioSplits.length === 1 && params.raw?.length < 15) splitKey = prioSplits[0];
    }
  }

  // Build injury-aware group caps and keyword filter
  const groupCaps = {};
  injuries.forEach(inj => {
    const caps = AP_INJURY_GROUP_CAPS[inj] || {};
    Object.entries(caps).forEach(([g, n]) => {
      groupCaps[g] = Math.min(groupCaps[g] ?? 99, n);
    });
  });
  const avoidKeywords = (avoidExercises || '').toLowerCase()
    .split(/[,;]/).map(s => s.trim()).filter(Boolean);

  const sessions = acBuildDetailedRoutine(splitKey, goal, level, tiempo, allExs, groupCaps, avoidKeywords, musclePriorities, params.count || null, ctx.log || []);
  if (!sessions.length || sessions.every(s => !s.exercises.length)) {
    return { type:'text', text:'No pude armar la rutina con los ejercicios disponibles. Prueba con otro tipo de split.' };
  }

  // ── Build explanatory intro ────────────────────────────────────────────────
  const reasonParts = [];

  // Goal
  reasonParts.push(`objetivo: ${goal}`);

  // Level
  reasonParts.push(`nivel ${level}`);

  // Muscle priorities
  if (musclePriorities.length > 0) {
    const labels = musclePriorities.slice(0,3).map(m => AP_MUS_MAP[m] || m).join(', ');
    reasonParts.push(`priorizando ${labels}`);
  }

  // Training days explanation
  reasonParts.push(`${days} días/sem → split ${splitKey.toUpperCase().replace('_',' ')}`);

  // Session duration
  if (tiempo) reasonParts.push(`~${tiempo} min por sesión`);

  let intro = `${acVariant('routine-intro', ['He generado este plan', 'Aquí tienes tu plan', 'Listo — te he preparado este plan'])} teniendo en cuenta tu ${reasonParts.join(', ')}.`;

  // Context adjustments
  const contextNotes = [];
  // Solo con historial real y un parón largo (≥14 días); nunca a usuarios nuevos.
  if (ctx.totalSessions > 0 && ctx.daysSinceLast >= 14) contextNotes.push(`Llevas ${ctx.daysSinceLast} días sin entrenar — empieza al 70–80% de tus cargas habituales.`);
  if (ctx.pushPullRatio > 1.5 && !['pull','legs'].includes(splitKey)) contextNotes.push(`Tu tracción está por debajo de tu empuje esta semana; prioriza espalda en tu próxima sesión.`);
  if (ctx.plateaus.length > 0) contextNotes.push(`Noto plateau en ${ctx.plateaus[0].name} (${ctx.plateaus[0].kg} kg). Intenta superar ese peso en al menos una serie.`);

  // Plateau alert: exercises kept in session because no alternative was available
  const plateauAlertExs = sessions.flatMap(s => s.exercises.filter(e => e.plateauAlert));
  if (plateauAlertExs.length > 0) {
    const names = [...new Set(plateauAlertExs.map(e => e.name))].slice(0, 2).join(' y ');
    const verb  = plateauAlertExs.length === 1 ? 'lleva' : 'llevan';
    contextNotes.push(`⚠ ${names} ${verb} varias sesiones sin progresar. He preloadeado las cargas anteriores — prueba bajar el RIR en 1 punto o introducir una variación del mismo patrón de movimiento.`);
  }

  // Injury explanations
  const injuryNotes = injuries
    .map(inj => AP_INJURY_WARNS[inj])
    .filter(Boolean);
  if (avoidExercises) injuryNotes.push(`He excluido ejercicios indicados por ti: ${avoidExercises}.`);

  if (contextNotes.length) intro += '\n\n' + contextNotes.join(' ');
  if (injuryNotes.length) intro += '\n\n⚠ Ajustes por tu perfil:\n' + injuryNotes.map(n => `• ${n}`).join('\n');

  // Save full AtlasRoutine as shared persistent entity
  const routineId = `routine-${Date.now()}`;
  const atlasRoutine = {
    id:         routineId,
    name:       `${splitKey.toUpperCase().replace('_',' ')} · ${goal}`,
    objective:  goal,
    frequency:  days,
    split:      splitKey,
    sessions,
    priorities: musclePriorities,
    createdAt:  Date.now(),
    source:     'coach',
  };
  arSave(atlasRoutine);

  return {
    type: 'routine',
    text: intro,
    sessions,
    builderPayload: sessions[0]?.exercises || [],
    routineId,
    routineName: atlasRoutine.name,
  };
}

function acResponseAnalysis(ctx, profile) {
  if (ctx.totalSessions === 0) {
    return { type:'text', text:'No tienes sesiones registradas. Guarda tu primera rutina en el Builder y podré analizar tu entrenamiento con datos reales.' };
  }

  const issues = [];

  // Push/pull balance
  if (ctx.pushVol > 0 && ctx.pullVol === 0) {
    issues.push({ sev:'warning', icon:'⊘', title:'Sin tracción esta semana', detail:`Has hecho ${ctx.pushVol} series de empuje pero 0 de tracción. Esto acumula tensión anterior en el manguito rotador.`, rec:'Añade remo o jalón a tu próxima sesión antes de volver a entrenar pecho.' });
  } else if (ctx.pushPullRatio > 1.5 && ctx.pushVol > 3) {
    issues.push({ sev:'warning', icon:'⊘', title:`Desequilibrio push/pull (${ctx.pushVol}:${ctx.pullVol})`, detail:`Ratio actual ${ctx.pushPullRatio.toFixed(1)}:1. El objetivo es 1:1 o incluso 1:1.2 en favor de la tracción para proteger los hombros.`, rec:'En tu próxima sesión prioriza espalda antes de pecho.' });
  } else if (ctx.pushVol > 0 && ctx.pullVol > 0) {
    issues.push({ sev:'good', icon:'✓', title:`Equilibrio push/pull correcto (${ctx.pushVol}:${ctx.pullVol})`, detail:'Buen ratio entre ejercicios de empuje y tracción. Sigue así.', rec:null });
  }

  // Plateau
  if (ctx.plateaus.length > 0) {
    const p = ctx.plateaus[0];
    issues.push({ sev:'info', icon:'ℹ', title:`Plateau detectado: ${p.name}`, detail:`${p.sessions} sesiones consecutivas con ${p.kg} kg sin progresión. Puede ser fatiga acumulada o que el ejercicio ya no es el estímulo óptimo.`, rec:'Prueba reducir el RIR 1 punto esta semana, o cambia a una variación del mismo patrón.' });
  }

  // Rest/absence
  if (ctx.daysSinceLast > 7) {
    issues.push({ sev:'info', icon:'ℹ', title:`${ctx.daysSinceLast} días sin entrenar`, detail: ctx.daysSinceLast > 14 ? 'Más de 2 semanas. El volumen muscular se mantiene bien hasta la semana 3, pero el rendimiento neuronal cae antes.' : 'Una semana de descanso puede ser un deload involuntario — eso no es malo.', rec: ctx.daysSinceLast > 12 ? 'Vuelve con un 30–40% menos de volumen en la primera sesión.' : 'Cuando retomes, empieza en el 80% de tus cargas habituales.' });
  }

  // Streak
  if (ctx.streak >= 4) {
    issues.push({ sev:'good', icon:'✓', title:`${ctx.streak} días de racha activa`, detail:'La constancia es el factor número uno del progreso a largo plazo. Es más importante que cualquier técnica avanzada.', rec:null });
  }

  // Low volume
  if (ctx.lowVolGroups.length > 0) {
    issues.push({ sev:'warning', icon:'⚠', title:`Volumen bajo en: ${ctx.lowVolGroups.join(', ')}`, detail:`Para crecer, lo habitual es acercarse al rango de 10–20 series semanales por grupo (los principiantes progresan bien con menos); estos grupos se están quedando por debajo. Repártelas en las sesiones que te encajen.`, rec:`Puedes añadir 2–3 series específicas de ${ctx.lowVolGroups[0]} en tu próxima sesión.` });
  }

  // Adherence
  if (ctx.adherenceRate < 0.65 && ctx.totalSessions > 5) {
    issues.push({ sev:'info', icon:'ℹ', title:`Adherencia: ${ctx.weekSessions}/${ctx.targetDays} días esta semana`, detail:'Estar por debajo del objetivo no es malo de forma puntual, pero si se repite puede frenar el progreso esperado.', rec:'Considera reducir el objetivo semanal a algo sostenible. 3 días consistentes > 5 días irregulares.' });
  }

  // Fatigue
  if (ctx.fatigueLevel === 'high') {
    issues.push({ sev:'warning', icon:'⚠', title:'Volumen semanal muy alto', detail:`${ctx.totalSetsWeek} series esta semana (media de ${ctx.avgSetsPerSess.toFixed(1)}/sesión).`, rec:'Una descarga: reduce claramente el número de series, deja más repeticiones en reserva y mantén los mismos ejercicios.' });
  }

  // Scientific panel: MRV violations
  if (ctx.sePanel?.mrvAlerts?.length > 0) {
    const a = ctx.sePanel.mrvAlerts[0];
    const overZone = a.zone === 'above_mrv';
    issues.push({
      sev: overZone ? 'warning' : 'info',
      icon: overZone ? '⚠' : 'ℹ',
      title: overZone
        ? `${a.name}: por encima del MRV (${a.current}/${a.mrv} series/sem)`
        : `${a.name}: cerca del MRV (${a.current}/${a.mrv} series/sem)`,
      detail: `El MRV para ${a.name} es un rango orientativo de volumen (en torno a ${a.mrv} series semanales). Con ${a.current} series estás ${overZone ? 'por encima — más volumen suele dar más resultado, pero con rendimientos decrecientes y más fatiga' : 'en la zona de alerta — al límite de lo recuperable'}.`,
      rec: `Reduce ${Math.max(1, a.current - a.mav)} series de ${a.name} la próxima semana y mantén la intensidad.`,
    });
  }

  // Scientific panel: fatigue score
  if (ctx.sePanel?.fatigue?.score >= 70) {
    const f = ctx.sePanel.fatigue;
    issues.push({
      sev: 'warning', icon: '⚠',
      title: `Fatiga acumulada alta: ${f.score}/100`,
      detail: `Score de fatiga calculado sobre volumen (${f.totalSets} series), frecuencia (${f.daysTraining} días) e intensidad estimada (~${f.avgIntPct}% 1RM). ${f.factors.slice(0,2).join('; ')}.`,
      rec: 'Un deload de 5–7 días al 50% del volumen habitual resolvería el exceso de fatiga sin perder masa muscular.',
    });
  }

  // ── Atlas Profile–specific checks ─────────────────────────────────────────
  // Injury risk check
  if (ctx.injuryRisks?.length > 0) {
    ctx.injuryRisks.forEach(risk => {
      issues.push({ sev:'warning', icon:'⚠', title:`Riesgo por limitación en ${risk.muscle}`, detail:`${risk.warn}. Este desequilibrio puede agravar la zona lesionada.`, rec:`Añade tracción antes de tu próxima sesión de empuje.` });
    });
  }

  // Muscle priority volume gap
  if (ctx.priorityGaps?.length > 0) {
    ctx.priorityGaps.forEach(gap => {
      issues.push({ sev:'warning', icon:'⊕', title:`${gap.label} — músculo prioritario con poco volumen`, detail:`Has marcado ${gap.label} como prioritario pero solo acumula ${gap.vol} series esta semana. Para hipertrofia se suele recomendar entre 10 y 20 series semanales por grupo muscular (los principiantes progresan bien con menos); puedes repartirlas en las sesiones que mejor te encajen.`, rec:`Puedes añadir una sesión de ${gap.label} o incluir 2–3 series extra en tu próximo entrenamiento.` });
    });
  }

  // Goal-specific check
  const objetivo = profile?.objetivo || '';
  if (objetivo.includes('grasa') && ctx.totalSetsWeek < 20 && ctx.weekSessions < 2) {
    issues.push({ sev:'info', icon:'ℹ', title:'Frecuencia baja para pérdida de grasa', detail:'Para pérdida de grasa el volumen semanal y la consistencia son clave. Con menos de 2 sesiones por semana el déficit calórico generado por el entrenamiento es mínimo.', rec:'Intenta mantener al menos 3 sesiones semanales, aunque sean más cortas.' });
  }

  // APE alerts — add any not already covered
  if (ctx.apePanel?.alerts?.length > 0) {
    const apeAlerts = ctx.apePanel.alerts.filter(a => a.type === 'frequency_drop' || a.type === 'notable_improvement');
    apeAlerts.forEach(a => {
      const sev = a.severity === 'success' ? 'good' : a.severity;
      issues.push({ sev, icon: a.severity === 'success' ? '✓' : '⚠', title: a.message, detail:'', rec:'' });
    });
  }

  const warns = issues.filter(i => i.sev === 'warning');
  const profileNote = profile ? ` (objetivo: ${profile.objetivo}, nivel ${profile.nivel})` : '';
  const summary = warns.length > 0
    ? `He revisado ${Math.min(ctx.totalSessions, 10)} sesiones${profileNote}. ${warns.length} punto${warns.length > 1 ? 's' : ''} a corregir.`
    : `He revisado ${Math.min(ctx.totalSessions, 10)} sesiones${profileNote}. Tu entrenamiento va bien.`;

  return {
    type: 'analysis',
    summary,
    issues,
    stats: {
      sessions: ctx.totalSessions, week: ctx.weekSessions, push: ctx.pushVol,
      pull: ctx.pullVol, streak: ctx.streak, fatigue: ctx.fatigueLevel,
      fatigueScore: ctx.sePanel?.fatigue?.score ?? null,
      mrvAlerts: ctx.sePanel?.mrvAlerts ?? [],
      topMuscles: ctx.sePanel?.topMuscles ?? [],
    },
    relatedArticles: ['frecuencia-entrenamiento', 'sobrecarga-progresiva', 'rpe-rir'],
  };
}

function acResponseInjury(text, memory) {
  const t = text.toLowerCase();
  const parts = ['hombro','rodilla','lumbar','espalda baja','muñeca','codo','tobillo','cadera','cuello','gemelo'];
  const found = parts.filter(p => t.includes(p));

  // No se guarda la zona de dolor en el perfil sin que el usuario lo decida.
  const part = found[0];
  if (!part) {
    return { type:'text', awaitContext:'injury-location', text:'¿Dónde exactamente? Cuéntame si duele durante el movimiento, después, o en reposo — eso cambia completamente el enfoque.' };
  }

  // Orientación general (sin pautas de ejercicios de readaptación): no puedo
  // valorar una lesión a distancia; la readaptación concreta la indica un
  // profesional sanitario.
  const advice = 'Como pauta general: baja la carga o reduce el rango en los movimientos que afecten a esa zona, evita todo lo que reproduzca el dolor y no entrenes con dolor.';
  const disclaimer = 'Esto es orientación general, no un diagnóstico: no puedo evaluar tu dolor ni tu lesión a distancia. Si el dolor persiste, es intenso o limita tu movimiento, consulta a un fisioterapeuta o médico antes de seguir entrenando esa zona.';
  const offer = acCoachOnlineOn() ? '\n\nY si quieres ayuda para adaptar tu entrenamiento mientras te recuperas, puedes entrenar con un coach.' : '';

  return { type:'text', text:`Sobre la molestia en ${part}:\n\n${advice}\n\n${disclaimer}${offer}`, link: acCoachLink() };
}

function acResponsePlateau(ctx) {
  const ps = ctx.progressSummary;
  const engineStagnant = ps?.stagnant || [];

  if (engineStagnant.length > 0) {
    const lines = [`He analizado tu historial completo. Detecto ${engineStagnant.length} ejercicio${engineStagnant.length !== 1 ? 's' : ''} sin progreso:\n`];
    engineStagnant.slice(0, 4).forEach(e => {
      lines.push(`• ${e.name} — ${e.sessions} sesiones en ${e.kg} kg (${e.trend === 'declining' ? 'en descenso' : 'estancado'})`);
    });
    lines.push(`\nCausas más frecuentes: (1) fatiga acumulada que enmascara tu fuerza real, (2) volumen insuficiente como estímulo.\n\nAcción recomendada — una descarga: reduce claramente el número de series, deja más repeticiones en reserva y mantén los mismos ejercicios.`);
    return { type:'text', text: lines.join('\n'), relatedArticles: ['deload', 'frecuencia-entrenamiento', 'sobrecarga-progresiva'] };
  }

  if (ctx.plateaus.length > 0) {
    const p = ctx.plateaus[0];
    return { type:'text', text:`Detecto plateau en ${p.name} — llevas ${p.sessions} sesiones en ${p.kg} kg.\n\nCausas más comunes: fatiga acumulada o volumen insuficiente como estímulo nuevo.\n\nSolución — una descarga: reduce claramente el número de series, deja más repeticiones en reserva y mantén los mismos ejercicios.`, relatedArticles: ['deload', 'sobrecarga-progresiva'] };
  }

  return { type:'text', text:'No detecto estancamientos en tu historial. Si lo percibes en un ejercicio concreto, dime cuál y lo analizo en detalle.' };
}

function acResponseProgramming(text, ctx) {
  const t = text.toLowerCase();
  if (/series|volumen/.test(t)) {
    const sePanel = ctx?.sePanel;
    const tracked = sePanel ? Object.values(sePanel.volumeStatus).filter(v => v.current > 0).sort((a, b) => b.current - a.current) : [];
    if (tracked.length > 0) {
      const zoneLabel = z => z === 'mev_to_mav' ? '✓ óptimo' : z === 'mav_to_mrv' ? '⚠ cerca MRV' : z === 'above_mrv' ? '⛔ sobre MRV' : '↓ bajo MEV';
      const lines = ['Tu volumen semanal actual por músculo:'];
      tracked.slice(0, 7).forEach(vs => {
        lines.push(`• ${vs.name}: ${vs.current} series — MEV ${vs.mev} · MAV ${vs.mav} · MRV ${vs.mrv}  ${zoneLabel(vs.zone)}`);
      });
      lines.push('\nMEV, MAV y MRV son rangos orientativos de volumen. Más volumen suele dar más resultado, pero con rendimientos decrecientes y más fatiga.');
      return { type:'text', text: lines.join('\n'), relatedArticles: ['frecuencia-entrenamiento', 'hipertrofia-mecanismos'] };
    }
    return { type:'text', text:'Para hipertrofia se suele recomendar entre 10 y 20 series semanales por grupo muscular; los principiantes progresan bien con menos. Empieza en la parte baja del rango y sube 1–2 series por semana si lo toleras y recuperas bien; superar de forma sostenida el rango sin recuperación suficiente puede ser contraproducente.', relatedArticles: ['frecuencia-entrenamiento', 'hipertrofia-mecanismos'] };
  }
  if (/frecuencia/.test(t)) {
    return { type:'text', text:'Lo determinante para la hipertrofia es el volumen semanal total: a volumen igualado, la frecuencia apenas cambia los resultados. La frecuencia es una herramienta para repartir ese volumen — cuando es alto, dividirlo en 2–3 sesiones por músculo ayuda a completarlo con calidad y menos fatiga por sesión. Para fuerza, repartir en más sesiones puede aportar algo. Elige la frecuencia que te permita cumplir tu volumen con buenas series.', relatedArticles: ['frecuencia-entrenamiento'] };
  }
  if (/fallo|al fallo/.test(t)) {
    return { type:'text', text:'El fallo muscular no es necesario para el crecimiento. Lo que importa es llegar a RIR 1–3 en las últimas series del ejercicio. Entrenar siempre al fallo acumula más fatiga de la que genera estímulo, especialmente en ejercicios compuestos. Úsalo puntualmente en ejercicios de aislamiento al final de la sesión.', relatedArticles: ['rpe-rir', 'hipertrofia-mecanismos'] };
  }
  if (/deload/.test(t)) {
    return { type:'text', text:'Señales de que te podría convenir una descarga: RPE subiendo para las mismas cargas, sueño peor de lo habitual, pérdida de motivación para entrenar, DOMS persistente entre sesiones. Una descarga: reduce claramente el número de series, deja más repeticiones en reserva y mantén los mismos ejercicios.', relatedArticles: ['deload', 'hrv-monitoreo'] };
  }
  return { type:'text', text: acVariant('prog-fallback', [
    '¿Qué aspecto de la programación quieres profundizar? Puedo explicar volumen óptimo, frecuencia, periodización, RIR/RPE, deload o cualquier concepto específico.',
    'Dime qué concepto quieres que desarrolle: volumen, frecuencia, RIR/RPE, fallo muscular, deload o periodización.',
  ]), relatedArticles:['frecuencia-entrenamiento', 'rpe-rir'] };
}

function acResponseRecovery(ctx) {
  const deload   = ctx.progressSummary?.deload;
  const sePanel  = ctx.sePanel;
  const fatigue  = sePanel?.fatigue;
  const recovery = sePanel?.recovery;

  if (deload?.needed) {
    const bullets      = deload.reasons.map(r => `• ${r}`).join('\n');
    const fatigueNote  = fatigue ? `\n\nFatiga acumulada calculada: ${fatigue.score}/100 (${fatigue.level}).` : '';
    return { type:'text', text:`Basándome en tu historial real, los datos apuntan a que te vendría bien una descarga:\n\n${bullets}${fatigueNote}\n\nUna descarga: reduce claramente el número de series, deja más repeticiones en reserva y mantén los mismos ejercicios.`, relatedArticles: ['deload', 'hrv-monitoreo', 'recuperacion-sueno'] };
  }

  if (fatigue && fatigue.score >= 70) {
    const factors = fatigue.factors.length ? `\n\nFactores detectados:\n${fatigue.factors.map(f => `• ${f}`).join('\n')}` : '';
    return { type:'text', text:`Tu score de fatiga es ${fatigue.score}/100 (${fatigue.level}).${factors}\n\nOpciones: (1) deload esta semana — 50% del volumen manteniendo los mismos pesos, (2) eliminar 1 sesión y compensar la siguiente semana. Recuerda que el músculo no crece durante el entrenamiento, sino en la recuperación.`, relatedArticles: ['deload', 'hrv-monitoreo', 'recuperacion-sueno'] };
  }

  if (ctx.fatigueLevel === 'high') {
    const scoreNote = fatigue ? ` (score: ${fatigue.score}/100)` : '';
    return { type:'text', text:`Tu volumen esta semana es alto (${ctx.totalSetsWeek} series, media de ${ctx.avgSetsPerSess.toFixed(0)}/sesión)${scoreNote}. Si el rendimiento está bajando o el sueño está peor, es señal de fatiga acumulada.\n\nOpciones: (1) deload esta semana — 50% del volumen manteniendo pesos, (2) eliminar 1 sesión y compensar la siguiente semana.` };
  }

  if (ctx.totalSessions > 0 && ctx.daysSinceLast > 2) {
    const readyNote = recovery ? ` Readiness estimado: ${recovery.readiness}/100 (${recovery.label}).` : '';
    return { type:'text', text:`Llevas ${ctx.daysSinceLast} días sin entrenar.${readyNote} Unas pocas semanas sin entrenar apenas afectan a la masa muscular. Cuando retomes, empieza en el 80% de tus cargas habituales.` };
  }

  const scoreNote = fatigue ? `\n\nFatiga actual: ${fatigue.score}/100 (${fatigue.level}). Readiness: ${recovery?.readiness ?? '—'}/100.` : '';
  return { type:'text', text:`Tu fatiga parece moderada o baja esta semana.${scoreNote}\n\nIndicadores de recuperación insuficiente: RPE más alto para las mismas cargas, DOMS persistente entre sesiones, peor calidad del sueño. Si no aparecen esas señales, continúa con el plan normal.`, relatedArticles: ['hrv-monitoreo', 'recuperacion-sueno'] };
}

function acResponseProgress(ctx) {
  const ps  = ctx.progressSummary;
  const ape = ctx.apePanel;

  if ((!ps && !ape) || ctx.log.length < 2) {
    return { type:'text', text:'Aún no hay suficiente historial para analizar tu progresión. Completa 2–3 sesiones registrando pesos y repeticiones en el Workout Player y podré darte un análisis real de tu evolución.', relatedArticles:['sobrecarga-progresiva'] };
  }

  const parts = [];

  // Atlas Progress Score
  if (ape?.score?.total > 0) {
    parts.push(`Atlas Progress Score: ${ape.score.total}/100 — ${ape.score.label}\nAdherencia ${ape.adherence?.pct ?? '–'}% · Consistencia ${ape.consistency?.activeWeeks ?? '–'}/8 semanas activas · Racha actual: ${ape.consistency?.streak ?? 0} días.`);
  }

  // Strength progression
  const topProg = ape?.strength?.topProgressing || ps?.topProgress || [];
  if (topProg.length > 0) {
    const list = topProg.slice(0, 3).map(e => `• ${e.exerciseName || e.name} — +${e.pct}% en ${e.sessions} registros`).join('\n');
    parts.push(`Ejercicios progresando:\n${list}`);
  }

  // Stagnant
  const stagnant = ape?.strength?.stagnant || ps?.stagnant || [];
  if (stagnant.length > 0) {
    const list = stagnant.slice(0, 2).map(e => `• ${e.exerciseName || e.name}`).join('\n');
    parts.push(`Sin progreso detectado en:\n${list}\nConsulta técnica o cambia el estímulo.`);
  }

  // Alerts from APE
  const alerts = (ape?.alerts || []).filter(a => a.severity === 'warning');
  if (alerts.length > 0) parts.push(`⚠ ${alerts[0].message}`);

  // Volume this week
  if (ps?.thisWeekVolume > 0) {
    parts.push(`Volumen esta semana: ${ps.thisWeekSets} series · ${ps.thisWeekVolume.toLocaleString('es-ES')} kg levantados.`);
  }

  if (parts.length === 0) {
    return { type:'text', text:'No hay datos de carga suficientes. Registra kg y repeticiones en cada ejercicio para que pueda analizar tu evolución.' };
  }

  parts.push('Revisa todos tus datos en /progreso o escríbeme sobre cualquier ejercicio concreto.');
  return { type:'text', text: parts.join('\n\n'), relatedArticles:['sobrecarga-progresiva'] };
}

function acResponseBuilderReview(ctx, profile) {
  const plan    = ctx.builderPlan;
  const routine = ctx.activeRoutine;
  const source  = plan || routine;

  if (!source) {
    return { type:'text', text:'No encuentro ninguna rutina guardada. Genera una desde aquí con "Crea mi rutina" y ábrela en Builder para que pueda analizarla.' };
  }

  const exercises        = plan?.exercises || routine?.sessions?.[0]?.exercises || [];
  const musclePriorities = profile?.musclePriorities || [];
  const injuries         = profile?.injuries          || [];

  const parts = [];

  const planName = plan?.name || routine?.name || 'tu rutina';
  parts.push(`He revisado ${planName} (${exercises.length} ejercicios).`);

  // Count sets per muscle keyword
  const setsByKw = {};
  exercises.forEach(ex => {
    const sets = ex.setsCount ?? (Array.isArray(ex.sets) ? ex.sets.length : 3);
    (ex.muscles?.primary || []).forEach(m => {
      const k = m.toLowerCase();
      setsByKw[k] = (setsByKw[k] || 0) + sets;
    });
  });

  const volFor = (kw) => Object.entries(setsByKw)
    .filter(([k]) => k.includes(kw))
    .reduce((t, [,v]) => t + v, 0);

  // Priority muscle check
  const MUS_KW = { chest:'pectoral', back:'dorsal', shoulders:'deltoid', arms:'bíceps', legs:'cuádricep', glutes:'glúteo' };
  musclePriorities.forEach(prio => {
    const kw   = MUS_KW[prio] || prio;
    const vol  = volFor(kw);
    const label = AP_MUS_MAP[prio] || prio;
    if (vol === 0) {
      parts.push(`${label} — marcado como prioritario — no aparece en la rutina. Añade al menos 2 ejercicios específicos.`);
    } else if (vol < 8) {
      parts.push(`${label} — solo ${vol} series en esta sesión. Para hipertrofia se suele recomendar entre 10 y 20 series semanales por grupo muscular (los principiantes progresan bien con menos); puedes repartirlas en las sesiones que mejor te encajen.`);
    } else {
      parts.push(`${label} — ${vol} series. Dentro del rango óptimo.`);
    }
  });

  // Injury conflict check
  const INJURY_PATTERNS = {
    shoulder: ['deltoid','tríceps','pectoral'],
    knee:     ['cuádricep','isquio'],
    lower_back: ['dorsal','glúteo'],
  };
  injuries.forEach(inj => {
    const muscleKws = INJURY_PATTERNS[inj] || [];
    const vol = muscleKws.reduce((t, kw) => t + volFor(kw), 0);
    if (vol > 12) {
      const warn = AP_INJURY_WARNS[inj] || `Tienes limitación en ${inj.replace(/_/g,' ')}`;
      parts.push(`⚠ ${warn} y la rutina tiene ${vol} series en esa zona. Considera reducirlo o añadir más trabajo antagónico.`);
    }
  });

  // Frequency note if multi-session routine
  if (routine?.sessions?.length > 1) {
    parts.push(`Tu plan ${routine.split?.toUpperCase() || ''} tiene ${routine.sessions.length} sesiones. Estás analizando la sesión que tienes en Builder — para ver el plan completo vuelve al Coach.`);
  }

  if (parts.length === 1) parts.push('No detecté puntos críticos. Continúa con el plan.');

  return { type:'text', text: parts.join('\n\n'), relatedArticles: ['sobrecarga-progresiva', 'frecuencia-entrenamiento', 'rpe-rir'] };
}

// ── Volume science response (MEV/MAV/MRV breakdown) ──────────────────────────
function acResponseVolumeScience(ctx, profile) {
  const panel = ctx.sePanel;
  if (!panel || !ctx.log?.length) {
    return { type:'text', text:'Sin sesiones registradas con datos de músculo. Registra tus entrenamientos en el Builder con pesos y el motor científico analizará tu volumen por músculo en tiempo real.', relatedArticles:['frecuencia-entrenamiento'] };
  }

  const tracked = Object.values(panel.volumeStatus)
    .filter(v => v.current > 0)
    .sort((a, b) => b.current - a.current);

  if (!tracked.length) {
    return { type:'text', text:'No detecto volumen por músculo esta semana. Asegúrate de registrar sesiones con ejercicios en el Builder.', relatedArticles:['frecuencia-entrenamiento'] };
  }

  const zoneLabel = z => z === 'mev_to_mav' ? '✓ MAV' : z === 'mav_to_mrv' ? '⚠ MRV' : z === 'above_mrv' ? '⛔ MRV+' : '↓ MEV';
  const frequency = panel.frequency;

  const lines = [`Análisis de volumen semanal (basado en ${ctx.totalSessions} sesiones):\n`];
  tracked.forEach(vs => {
    const freq  = frequency[vs.key] ? `${frequency[vs.key]}×/sem` : '';
    const zone  = zoneLabel(vs.zone);
    lines.push(`• ${vs.name}: ${vs.current} series ${freq ? `(${freq})` : ''} — ${zone} [MEV ${vs.mev} · MAV ${vs.mav} · MRV ${vs.mrv}]`);
  });

  if (panel.mrvAlerts.length) {
    lines.push(`\n⚠ Alertas MRV:`);
    panel.mrvAlerts.forEach(a => {
      const excess = a.current - a.mav;
      lines.push(`• ${a.name}: ${a.current} series — reduce ${excess} series para volver a rango MAV.`);
    });
  }

  if (panel.belowMEV.length) {
    lines.push(`\n↓ Por debajo de MEV (estímulo insuficiente):`);
    panel.belowMEV.slice(0, 4).forEach(v => {
      lines.push(`• ${v.name}: ${v.current}/${v.mev} series — añade ${v.gap} series más.`);
    });
  }

  lines.push(`\nFatiga estimada: ${panel.fatigue.score}/100 (${panel.fatigue.level}).`);

  return {
    type: 'text',
    text: lines.join('\n'),
    relatedArticles: ['frecuencia-entrenamiento', 'hipertrofia-mecanismos', 'deload'],
  };
}

// ── Last-session analysis ─────────────────────────────────────────────────────
// Post-session breakdown: volume per group, comparison with the previous
// similar session, progressions/regressions, and a suggestion for the next one.
function acResponseSessionAnalysis(ctx) {
  const log  = ctx.log || [];
  const last = log[0];
  if (!last) {
    return { type:'text', text:'Todavía no tienes ninguna sesión registrada. Completa un entrenamiento desde el Builder o el player y podré analizarlo aquí.' };
  }

  const lines = [];
  // Solo cuentan los ejercicios realmente realizados (con alguna serie hecha);
  // los logs ya guardan únicamente las series completadas.
  const exs = (last.exercises || []).filter(ex => (ex.sets || []).length > 0);
  // Título sin segmentos repetidos ("Full Body · Full Body · Día 1" → "Full Body · Día 1").
  const _seen = new Set();
  const name = [last.routineName, last.sessionName].filter(Boolean).join(' · ')
    .split(' · ').map(s => s.trim())
    .filter(s => { const k = s.toLowerCase(); if (!k || _seen.has(k)) return false; _seen.add(k); return true; })
    .join(' · ');
  const when = last.date === new Date().toDateString() ? 'hoy'
             : last.date === new Date(Date.now() - 86400000).toDateString() ? 'ayer'
             : last.date;

  let totalSets = 0, totalVolume = 0;
  const groupVol = {};
  exs.forEach(ex => {
    const sets = ex.sets || [];
    totalSets += sets.length;
    sets.forEach(st => { totalVolume += (parseFloat(st.kg) || 0) * (parseInt(st.reps) || 0); });
    const g = (Array.isArray(ex.muscles) ? ex.muscles[0] : ex.muscles?.primary?.[0]) || ex.group || 'otros';
    groupVol[g] = (groupVol[g] || 0) + sets.length;
  });
  const hasLoad = totalVolume > 0;

  lines.push(`📊 Análisis de tu última sesión (${when}${name ? ` — ${name}` : ''}):`);
  lines.push(`${exs.length} ${exs.length === 1 ? 'ejercicio' : 'ejercicios'} · ${totalSets} ${totalSets === 1 ? 'serie' : 'series'} · ${Math.round(totalVolume).toLocaleString('es-ES')} kg de volumen${last.duration ? ` · ${Math.max(1, Math.round(last.duration / 60))} min` : ''}.`);

  const gv = Object.entries(groupVol).sort((a, b) => b[1] - a[1]);
  if (gv.length) lines.push('Volumen por grupo: ' + gv.map(([g, n]) => `${g} ${n} ${n === 1 ? 'serie' : 'series'}`).join(' · ') + '.');

  // Find the previous comparable session (≥40% exercise-name overlap)
  const currNames = new Set(exs.map(e => e.name));
  let prev = null;
  for (let i = 1; i < log.length; i++) {
    const overlap = (log[i].exercises || []).filter(e => currNames.has(e.name)).length;
    if (overlap >= Math.min(2, Math.ceil(currNames.size * 0.4))) { prev = log[i]; break; }
  }

  if (prev) {
    const prevMap = {};
    (prev.exercises || []).forEach(ex => {
      const kgs = (ex.sets || []).map(s => parseFloat(s.kg) || 0).filter(k => k > 0);
      if (kgs.length) prevMap[ex.name] = Math.max(...kgs);
    });
    const ups = [], downs = [], sames = [];
    exs.forEach(ex => {
      const kgs = (ex.sets || []).map(s => parseFloat(s.kg) || 0).filter(k => k > 0);
      if (!kgs.length || prevMap[ex.name] === undefined) return;
      const d = +(Math.max(...kgs) - prevMap[ex.name]).toFixed(1);
      if (d > 0) ups.push(`${ex.name} +${d} kg`);
      else if (d < 0) downs.push(`${ex.name} ${d} kg`);
      else sames.push(ex.name);
    });
    lines.push(`Comparado con tu sesión similar del ${prev.date}:`);
    if (ups.length)   lines.push(`↑ Progresiones: ${ups.join(', ')}.`);
    if (sames.length) lines.push(`= Sin cambio: ${sames.slice(0, 4).join(', ')}${sames.length > 4 ? '…' : ''}.`);
    if (downs.length) lines.push(`↓ Retrocesos: ${downs.join(', ')}. Si se repite, revisa descanso y nutrición.`);
    if (!ups.length && !downs.length && sames.length) lines.push('Cargas estables — en la próxima intenta añadir 1-2 reps o subir el peso del primer ejercicio.');
  } else {
    lines.push('No encuentro una sesión previa comparable — cuando repitas ejercicios similares podré medir tu progresión.');
  }

  if (ctx.plateaus?.length) {
    lines.push(`💡 Para la próxima: ${ctx.plateaus[0].name} lleva ${ctx.plateaus[0].sessions} sesiones en ${ctx.plateaus[0].kg} kg — prueba una variante del mismo patrón o cambia el rango de reps.`);
  } else if (prev) {
    lines.push('💡 Para la próxima: mantén lo que progresa y sube ~2,5 kg donde completaste todas las series en el rango objetivo.');
  } else if (!hasLoad) {
    lines.push('💡 Registra kg y reps en cada serie para que pueda darte sugerencias de progresión precisas.');
  } else {
    // (5) Sin "prev", la línea de "cuando repitas…" ya se añadió arriba (una sola vez).
    lines.push('💡 Buen registro.');
  }

  return { type:'text', text: lines.join('\n') };
}

// ── Master response generator ─────────────────────────────────────────────────
// `pending` carries conversational memory (e.g. 'routine-split' after we asked
// "¿Qué tipo de rutina quieres?") so a short reply is read as the answer.
function acGenerateSmartResponse(userText, state, profile, memory, allExs, pending, convo) {
  const params  = acExtractParams(userText);
  // (2) Memoria de conversación: si el usuario ya dijo nivel, objetivo o días en
  // mensajes anteriores de esta conversación, se reutilizan en lugar de volver a
  // los valores por defecto. Lo que diga ahora tiene prioridad.
  const prevConvo = (convo && typeof convo === 'object') ? convo : {};
  if (!params.nivel && prevConvo.nivel) params.nivel = prevConvo.nivel;
  if (!params.goal  && prevConvo.goal)  params.goal  = prevConvo.goal;
  if (!params.dias  && prevConvo.dias)  params.dias  = prevConvo.dias;
  const convoParams = {
    nivel: params.nivel || prevConvo.nivel || null,
    goal:  params.goal  || prevConvo.goal  || null,
    dias:  params.dias  || prevConvo.dias  || null,
  };
  const ctx     = acBuildRichContext(state, profile);
  let intent    = acDetectIntent(userText);

  // ── Conversational memory ────────────────────────────────────────────────
  // `pending` is set when the Coach's previous message asked something, so a
  // short reply is read as the answer to that question, not in isolation.
  const nrm = acNorm(userText);

  // We just asked which split. Interpret an ambiguous or split-shaped reply as
  // the answer instead of evaluating it alone.
  if (pending === 'routine-split') {
    const answersSplit = !!(params.split || params.target || params.dias) ||
      /^\s*(no se|cualquiera|el que sea|tu decides|da igual|recomienda|lo que (sea|creas|veas|mejor)|elige|escoge)/.test(nrm);
    // routine + general are the ambiguous buckets; a clear other intent (analysis,
    // injury, programming…) still escapes so we never trap the user.
    if (answersSplit || intent === 'routine' || intent === 'general') intent = 'routine';
  }

  // We asked the user to choose (honest fallback). Route their short reply.
  if (pending === 'clarify') {
    if      (/rutina|entren|plan|split|ppl|full ?body|upper|lower|push|pull|legs|piern|pecho|espalda|hombro|brazo|gluteo/.test(nrm)) intent = 'routine';
    else if (/ejercicio|tecnica|ejecucion|como se (hace|ejecuta)|forma|postura|agarre/.test(nrm)) intent = 'exercise-doubt';
    else if (/aula|articulo|leer|lectura|contenido|lecci[oó]n|estudio|ciencia/.test(nrm))
      return { type:'text', text: acVariant('aula-pointer', [
        'En el Aula tienes artículos con base científica. Estos son un buen punto de partida:',
        'El Aula reúne el contenido con evidencia. Te dejo algunos para empezar:',
      ]), relatedArticles:['frecuencia-entrenamiento', 'sobrecarga-progresiva', 'hipertrofia-mecanismos'] };
    // else: let the freshly detected intent stand — never trap the user.
  }

  // We asked "¿dónde te duele?" — a short body-part reply is the answer.
  if (pending === 'injury-location' && (intent === 'general' || intent === 'injury')) intent = 'injury';

  const response = (() => {
    switch (intent) {
      case 'greeting':    return acResponseGreeting(ctx, profile, memory);
      case 'routine':     return acResponseRoutine(params, ctx, profile, memory, allExs);
      case 'analysis':    return acResponseAnalysis(ctx, profile);
      case 'session-analysis': return acResponseSessionAnalysis(ctx);
      case 'injury':      return acResponseInjury(userText, memory);
      case 'nutrition':   return acResponseNutrition();
      case 'personal-coaching': return acResponsePersonalCoaching();
      case 'exercise-doubt': return acResponseExerciseDoubt(userText, allExs);
      case 'plateau':     return acResponsePlateau(ctx);
      case 'programming':    return acResponseProgramming(userText, ctx);
      case 'recovery':       return acResponseRecovery(ctx);
      case 'volume-science':  return acResponseVolumeScience(ctx, profile);
      case 'builder-review':  return acResponseBuilderReview(ctx, profile);
      case 'progress-check':  return acResponseProgress(ctx);
      case 'profile':
        return { type:'text', text:'Puedes actualizar tu perfil en el panel inferior del sidebar (objetivo, nivel, días/semana, tiempo y equipamiento). Cuando lo guardes, usaré esos datos para todas mis recomendaciones.' };
      default:
        return acFallbackResponse();
    }
  })();
  // Propaga la memoria de conversación para los siguientes mensajes.
  if (response && typeof response === 'object' && !Array.isArray(response)) response._convo = convoParams;
  return response;
}

// ── Honest fallback — never go mute ────────────────────────────────────────────
// When the Coach can't map the message to a block, it says so plainly and offers
// clear options, then waits for the answer (pending 'clarify').
function acFallbackResponse() {
  return {
    type: 'text',
    awaitContext: 'clarify',
    text: acVariant('fallback', [
      'No estoy seguro de haberte entendido del todo. ¿Quieres que te recomiende una rutina, que resolvamos una duda sobre un ejercicio, o ver contenido del Aula?',
      'Puede que no lo haya captado bien — aún estoy en beta. Dime si buscas una rutina, una duda de técnica, un análisis de tu progreso, o contenido del Aula.',
      'No termino de entenderte. Puedo generarte una rutina, aclarar la técnica de un ejercicio, analizar tu historial o enseñarte artículos del Aula. ¿Qué prefieres?',
    ]),
  };
}

// ── Fuera de alcance — honestidad ──────────────────────────────────────────────
// La app no diseña dietas ni hace seguimiento individual. Con el servicio de
// coaching activo lo ofrece; desactivado, recomienda un profesional titulado.
function acResponseNutrition() {
  const base = 'La nutrición y las dietas personalizadas quedan fuera de lo que puedo hacer: no diseño planes de alimentación ni calculo tus calorías o macros. Para eso lo ideal es un dietista-nutricionista.';
  return {
    type: 'text',
    text: acCoachOnlineOn() ? `${base} Si quieres acompañamiento con tu entrenamiento, puedes entrenar con un coach.` : base,
    link: acCoachLink(),
  };
}
function acResponsePersonalCoaching() {
  const base = 'El seguimiento personal continuado queda fuera de lo que hago: soy una guía dentro de la app, no puedo acompañarte de forma individual ni revisar en persona cómo entrenas.';
  return {
    type: 'text',
    text: acCoachOnlineOn() ? `${base} Si buscas eso, puedes entrenar con un coach.` : `${base} ${AC_PRO_REF}`,
    link: acCoachLink(),
  };
}

// ── Exercise-doubt response ─────────────────────────────────────────────────────
// Identifies the exercise via the fuzzy search (accent/typo-tolerant) and gives a
// scripted technique cue + points to the Builder demo. Never goes mute: if it
// can't identify the exercise it offers "¿quisiste decir…?" suggestions.
function acResponseExerciseDoubt(userText, allExs) {
  const list = allExs || [];
  let matches = [];
  try { matches = ExerciseService.searchIn(list, userText) || []; } catch {}
  if (!matches.length) {
    // Strip technique/filler words so the search focuses on the exercise name.
    const cleaned = acNorm(userText)
      .replace(/\b(tecnica|ejecucion|como|se|hace|hago|hacer|ejecuta|realiza|forma|correcta|postura|agarre|bien|el|la|los|las|de|del|un|una|estoy|haciendo|lo|mi|es|para|con|en|que|cual|cuando)\b/g, ' ')
      .replace(/\s+/g, ' ').trim();
    if (cleaned) { try { matches = ExerciseService.searchIn(list, cleaned) || []; } catch {} }
  }

  const ex = matches[0];
  if (ex) {
    const cue  = AC_PATTERN_CUE[ex.pattern] || 'controla la fase excéntrica, mantén el core firme y trabaja en rango completo sin comprometer la postura.';
    const prim = (ex.muscles?.primary || []).slice(0, 2).join(', ');
    const lead = acVariant('ex-doubt', ['Claves de técnica', 'Puntos clave', 'Para hacerlo bien']);
    const techOffer = acCoachOnlineOn()
      ? 'Si quieres que alguien te evalúe y te acompañe, puedes entrenar con un coach.'
      : AC_PRO_REF;
    return {
      type: 'text',
      text: `${ex.name}${prim ? ` — trabaja ${prim}.` : '.'}\n\n${lead}: ${cue}\n\nÁbrelo en el Builder para ver la demostración animada y añadirlo a tu rutina.\n\nEso sí: no veo cómo lo ejecutas tú ni puedo corregir tu técnica. ${techOffer}`,
      relatedArticles: ['sobrecarga-progresiva', 'rpe-rir'],
      link: acCoachLink(),
    };
  }

  let sugg = [];
  try { sugg = ExerciseService.suggest(userText, 3) || []; } catch {}
  const hint = sugg.length ? ` ¿Te refieres a: ${sugg.map(s => s.name).join(', ')}?` : '';
  const techOffer2 = acCoachOnlineOn()
    ? 'Eso sí: no veo cómo lo ejecutas tú; si quieres que alguien te evalúe y te acompañe, puedes entrenar con un coach.'
    : `Eso sí: no veo cómo lo ejecutas tú. ${AC_PRO_REF}`;
  return {
    type: 'text',
    awaitContext: 'clarify',
    text: `Puedo darte las claves generales de técnica de un ejercicio, pero no estoy seguro de cuál me preguntas.${hint}\n\nDime el nombre del ejercicio o búscalo en el Builder para ver su demostración. ${techOffer2}`,
    link: acCoachLink(),
  };
}

// ── Dynamic context chips ─────────────────────────────────────────────────────
function acGetDynamicChips(ctx, profile) {
  const chips = [];
  const musclePriorities = profile?.musclePriorities || [];
  const injuries = profile?.injuries || [];

  if (ctx.daysSinceLast > 4 && ctx.daysSinceLast < 99) chips.push(`¿Retomamos hoy?`);
  if (ctx.plateaus.length > 0) chips.push(`Estancado en ${ctx.plateaus[0].name.split(' ')[0]}`);
  if (ctx.sePanel?.mrvAlerts?.length > 0) chips.push(`${ctx.sePanel.mrvAlerts[0].name} cerca del MRV`);
  if (ctx.injuryRisks?.length > 0) chips.push('Ajustar rutina por lesión');
  else if (ctx.priorityGaps?.length > 0) chips.push(`Más volumen de ${ctx.priorityGaps[0].label}`);
  else if (ctx.pushPullRatio > 1.5) chips.push('Necesito más tracción');
  if (ctx.fatigueLevel === 'high' || ctx.sePanel?.fatigue?.score >= 65) chips.push('¿Debo hacer deload?');
  if (ctx.totalSessions === 0) chips.push('Crear mi plan de entrenamiento');
  if (musclePriorities.length > 0) {
    const prio = musclePriorities[0];
    chips.push(`Rutina para ${AP_MUS_MAP[prio] || prio}`);
  }
  chips.push(`Analizar mi historial`);
  chips.push(`Rutina de ${profile?.objetivo || 'hoy'}`);
  return [...new Set(chips)].slice(0, 4);
}

// ── Welcome message ───────────────────────────────────────────────────────────
function acWelcomeMessage(state, profile, memory) {
  const ctx = acBuildRichContext(state, profile);
  const injuries = profile?.injuries || [];
  const musclePriorities = profile?.musclePriorities || [];

  if (ctx.totalSessions === 0) {
    if (!profile) return 'Hola. Soy Atlas Coach. ¿En qué te ayudo — una rutina, una duda de un ejercicio o analizar tu progreso?';
    const lines = [`Hola. He cargado tu perfil: ${profile.objetivo}, nivel ${profile.nivel}, ${profile.dias} días/sem.`];
    if (injuries.length) lines.push(`Tengo en cuenta tus limitaciones: ${injuries.map(i => i.replace(/_/g,' ')).join(', ')}.`);
    if (musclePriorities.length) lines.push(`Grupos prioritarios: ${musclePriorities.map(m => AP_MUS_MAP[m] || m).join(', ')}.`);
    lines.push('¿Genero tu primer plan?');
    return lines.join(' ');
  }

  const parts = [];
  if (ctx.daysSinceLast === 0) parts.push('Entrenaste hoy.');
  else if (ctx.daysSinceLast === 1) parts.push('Entrenaste ayer.');
  else if (ctx.daysSinceLast < 99) parts.push(`Llevas ${ctx.daysSinceLast} días sin entrenar.`);
  if (ctx.streak >= 3) parts.push(`${ctx.streak} días de racha.`);
  if (ctx.injuryRisks?.length > 0) parts.push(`⚠ ${ctx.injuryRisks[0].warn}.`);
  else if (ctx.priorityGaps?.length > 0) parts.push(`${ctx.priorityGaps[0].label} — prioritario — lleva poco volumen esta semana.`);
  else if (ctx.pushPullRatio > 1.5 && ctx.pushVol > 3) parts.push(`Más empuje que tracción esta semana (${ctx.pushVol}:${ctx.pullVol}).`);
  else if (ctx.plateaus.length > 0) parts.push(`Posible plateau en ${ctx.plateaus[0].name}.`);
  else if (ctx.lowVolGroups.length > 0) parts.push(`${ctx.lowVolGroups[0]} con poco volumen esta semana.`);
  else if (ctx.fatigueLevel === 'high') parts.push('Volumen semanal alto — considera un deload.');
  if (memory?.lesiones?.length > 0) parts.push(`Recuerdo molestias en ${memory.lesiones.slice(0,2).join(' y ')}.`);
  parts.push(acVariant('greet-close', ['¿En qué te ayudo?', '¿Por dónde quieres empezar?', 'Dime en qué te echo una mano.']));
  return parts.join(' ');
}

// First message of a new chat: presents the Coach's scope naturally (with the
// in-line "entrenar con un coach" link) and then the context-aware greeting.
function acWelcomeContent(state, profile, memory) {
  const scopeIntro = 'Te ayudo a elegir una rutina según tu perfil y objetivo, a entender el porqué de cada decisión y a revisar cómo vas con tu historial.';
  const scope = acCoachOnlineOn()
    ? `${scopeIntro} No veo cómo ejecutas los ejercicios ni sustituyo a un entrenador: si quieres que alguien te evalúe y te acompañe, puedes entrenar con un coach.`
    : `${scopeIntro} No veo cómo ejecutas los ejercicios ni sustituyo a un entrenador. ${AC_PRO_REF}`;
  const ctx = acBuildRichContext(state, profile);
  // Skip the generic "Soy Atlas Coach…" tail when there's no profile/history —
  // the scope paragraph already covers it; otherwise append the personalized line.
  const skipContext = !profile && ctx.totalSessions === 0;
  const context = skipContext ? '' : acWelcomeMessage(state, profile, memory).replace(/^Hola\.\s*/, '');
  const text = context ? `Hola. ${scope}\n\n${context}` : `Hola. ${scope}`;
  return { type: 'text', text, link: acCoachLink() };
}

// ── UI Components ─────────────────────────────────────────────────────────────

// Invitation card shown on first visit — non-blocking, dismissable.
// On mobile it collapses to a single compact line so it never dominates the chat.
function AtlasProfileCard({ onStart, onDismiss }) {
  const isMobile = useIsMobile();
  if (isMobile) {
    return (
      <div style={{
        display:'flex', alignItems:'center', gap:10,
        borderRadius:12, border:'1px solid rgba(59,130,246,0.28)',
        background:'linear-gradient(135deg, rgba(10,20,50,0.98) 0%, rgba(6,13,24,0.98) 100%)',
        padding:'10px 12px', marginBottom:16, animation:'fadeIn .3s ease',
      }}>
        <span style={{ fontFamily:'Inter,system-ui', fontSize:13, color:'rgba(232,237,248,0.72)', flex:1, minWidth:0, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
          Personaliza tu experiencia Atlas
        </span>
        <button onClick={onStart} style={{ flexShrink:0, minHeight:36, padding:'0 14px', borderRadius:9, border:'none', cursor:'pointer', background:'#2563EB', color:'#fff', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700 }}>Crear</button>
        <button onClick={onDismiss} aria-label="Descartar" style={{ flexShrink:0, width:32, height:32, borderRadius:8, border:'none', cursor:'pointer', background:'transparent', color:'rgba(232,237,248,0.3)', fontSize:15 }}>✕</button>
      </div>
    );
  }
  return (
    <div style={{
      borderRadius: 16,
      border: '1px solid rgba(59,130,246,0.28)',
      background: 'linear-gradient(135deg, rgba(10,20,50,0.98) 0%, rgba(6,13,24,0.98) 100%)',
      padding: '22px 24px',
      marginBottom: 28,
      animation: 'fadeIn .35s ease',
      position: 'relative',
    }}>
      {/* Dismiss */}
      <button onClick={onDismiss} aria-label="Cerrar" title="Cerrar" style={{
        position:'absolute', top:10, right:12,
        background:'none', border:'none', cursor:'pointer',
        fontFamily:'Inter,system-ui', fontSize:15, color:'rgba(232,237,248,0.45)',
        lineHeight:1, padding:6,
      }}>✕</button>

      {/* Badge */}
      <div style={{ display:'inline-flex', alignItems:'center', gap:6, marginBottom:14,
        padding:'3px 10px', borderRadius:999,
        background:'rgba(59,130,246,0.10)', border:'1px solid rgba(59,130,246,0.22)' }}>
        <span style={{ width:5, height:5, borderRadius:'50%', background:'#3B82F6', display:'block' }} />
        <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700,
          color:'#93C5FD', letterSpacing:1.2 }}>PERFIL ATLAS</span>
      </div>

      <div style={{ fontFamily:'Inter,system-ui', fontSize:16, fontWeight:700,
        color:'#E8EDF8', marginBottom:10, letterSpacing:-0.3 }}>
        Personaliza tu experiencia Atlas
      </div>
      <p style={{ fontFamily:'Inter,system-ui', fontSize:13, color:'rgba(232,237,248,0.55)',
        lineHeight:1.65, margin:'0 0 20px', maxWidth:480 }}>
        Atlas Coach puede ofrecer recomendaciones más precisas si conoce tus objetivos, experiencia y disponibilidad de entrenamiento. Completar tu Perfil Atlas solo te llevará unos minutos y permitirá adaptar futuras recomendaciones a tus necesidades.
      </p>

      <div style={{ display:'flex', gap:10 }}>
        <button onClick={onStart} style={{
          padding:'10px 20px', borderRadius:10, border:'none', cursor:'pointer',
          background:'#2563EB', color:'#fff',
          fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, letterSpacing:-0.2,
          boxShadow:'0 4px 18px -4px rgba(59,130,246,0.45)',
        }}>Crear Perfil Atlas</button>
        <button onClick={onDismiss} style={{
          padding:'10px 18px', borderRadius:10, cursor:'pointer',
          background:'rgba(255,255,255,0.05)', border:'1px solid rgba(255,255,255,0.10)',
          color:'rgba(232,237,248,0.45)',
          fontFamily:'Inter,system-ui', fontSize:13, fontWeight:600,
        }}>Ahora no</button>
      </div>
    </div>
  );
}

const OBJ_SHORT = { muscle:'Hipertrofia', fat_loss:'Perder grasa', recomp:'Recomposición', performance:'Rendimiento', health:'Salud' };
const EXP_SHORT = { beginner:'Principiante', intermediate:'Intermedio', advanced:'Avanzado' };

// Single, discreet profile prompt shown in the Coach when the Atlas Profile is
// incomplete. Replaces the old big card + sidebar panel + greeting nag — the
// profile is configured in one canonical place (the Perfil section), which this
// links to. One line, never dominates the chat.
function AcProfileNudge({ onGoProfile }) {
  return (
    <button onClick={onGoProfile} style={{
      display:'flex', alignItems:'center', gap:10, width:'100%', textAlign:'left',
      marginBottom:18, padding:'10px 12px', borderRadius:11, cursor:'pointer',
      border:'1px solid rgba(59,130,246,0.20)', background:'rgba(59,130,246,0.05)',
    }}>
      <span aria-hidden style={{ width:6, height:6, borderRadius:'50%', background:'#F59E0B', flexShrink:0 }} />
      <span style={{ flex:1, minWidth:0, fontFamily:'Inter,system-ui', fontSize:12.5, lineHeight:1.5, color:'rgba(232,237,248,0.62)' }}>
        Completa tu <b style={{ color:AC.text, fontWeight:700 }}>Perfil</b> para recibir recomendaciones según tu perfil.
      </span>
      <span style={{ flexShrink:0, fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, color:'#93C5FD' }}>Ir a Perfil →</span>
    </button>
  );
}

function AtlasProfileStatus({ profile, onEdit }) {
  const chips = [
    OBJ_SHORT[profile.objective] || profile.objective,
    EXP_SHORT[profile.experience] || profile.experience,
    profile.trainingDays ? `${profile.trainingDays} días/sem` : null,
  ].filter(Boolean);

  return (
    <div style={{
      borderRadius: 14,
      border: '1px solid rgba(34,197,94,0.22)',
      background: 'rgba(22,163,74,0.06)',
      padding: '14px 18px',
      marginBottom: 28,
      display: 'flex',
      alignItems: 'center',
      gap: 14,
      flexWrap: 'wrap',
      animation: 'fadeIn .3s ease',
    }}>
      <div style={{ display:'flex', alignItems:'center', gap:7, flexShrink:0 }}>
        <span style={{ width:7, height:7, borderRadius:'50%', background:'#4ADE80', display:'block' }} />
        <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700, color:'#4ADE80', letterSpacing:1.2 }}>PERFIL ATLAS ACTIVO</span>
      </div>
      <div style={{ display:'flex', gap:6, flex:1, flexWrap:'wrap' }}>
        {chips.map(c => (
          <span key={c} style={{ padding:'3px 10px', borderRadius:6, background:'rgba(74,222,128,0.10)', border:'1px solid rgba(74,222,128,0.20)', fontFamily:'Inter,system-ui', fontSize:11, fontWeight:600, color:'rgba(232,237,248,0.70)' }}>{c}</span>
        ))}
      </div>
      <button onClick={onEdit} style={{
        padding:'6px 14px', borderRadius:8, cursor:'pointer',
        background:'transparent', border:'1px solid rgba(255,255,255,0.14)',
        color:'rgba(232,237,248,0.50)', fontFamily:'Inter,system-ui', fontSize:11, fontWeight:700,
        flexShrink:0,
      }}>Editar perfil</button>
    </div>
  );
}

// Renders one onboarding step inside the chat bubble
function AcOnboardingStep({
  step, answered, pendingMulti, pendingInput, pendingInputs, pendingNote,
  onAnswer, onMultiToggle, onMultiConfirm, onInputChange, onInputConfirm,
  onBodyMetricsChange, onBodyMetricsConfirm, onNoteChange, onSkip,
}) {
  const isDone      = answered !== null && answered !== undefined;
  const { options = [], multi, maxSelect = 3, inputType, skippable, hasNoteField, noneOption } = step;

  const chipStyle = (sel, dim) => ({
    padding:'5px 12px', borderRadius:8,
    fontFamily:'Inter,system-ui', fontSize:12, fontWeight:600,
    background: sel ? 'rgba(59,130,246,0.18)' : 'transparent',
    border: `1px solid ${sel ? 'rgba(59,130,246,0.40)' : 'rgba(255,255,255,0.07)'}`,
    color: sel ? '#93C5FD' : dim ? 'rgba(232,237,248,0.20)' : 'rgba(232,237,248,0.25)',
  });

  const btnSty = { padding:'8px 16px', borderRadius:9, border:'none', cursor:'pointer', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700 };
  const skipBtn = skippable && !isDone ? (
    <button onClick={onSkip} style={{ ...btnSty, background:'transparent', border:'1px solid rgba(255,255,255,0.10)', color:'rgba(232,237,248,0.35)', marginLeft:8 }}>Saltar</button>
  ) : null;

  // On-device privacy note for optional health/body fields.
  const deviceNote = (step.onDeviceNote && !isDone) ? (
    <div style={{ display:'flex', alignItems:'center', gap:6, marginTop:10, fontFamily:'Inter,system-ui', fontSize:11, color:'rgba(232,237,248,0.42)', lineHeight:1.5 }}>
      <span aria-hidden>🔒</span> Opcional. Se guarda solo en tu dispositivo, nunca se envía a ningún servidor.
    </div>
  ) : null;

  // ── Answered state (read-only chips) ────────────────────────────────────────
  if (isDone) {
    if (inputType === 'number') {
      return <div style={{ marginTop:8, fontFamily:'ui-monospace,Menlo,monospace', fontSize:13, color:'#93C5FD' }}>{answered} {step.unit}</div>;
    }
    if (inputType === 'body-metrics') {
      const h = answered?.height, w = answered?.weight;
      return <div style={{ marginTop:8, fontFamily:'ui-monospace,Menlo,monospace', fontSize:13, color:'#93C5FD' }}>{[h && `${h} cm`, w && `${w} kg`].filter(Boolean).join(' · ') || 'Omitido'}</div>;
    }
    if (inputType === 'text') {
      return <div style={{ marginTop:8, fontFamily:'Inter,system-ui', fontSize:12, color:'#93C5FD', fontStyle: answered ? 'normal' : 'italic', opacity: answered ? 1 : 0.4 }}>{answered || 'Omitido'}</div>;
    }
    if (answered === null) {
      return <div style={{ marginTop:8, fontFamily:'Inter,system-ui', fontSize:12, color:'rgba(232,237,248,0.30)', fontStyle:'italic' }}>Omitido</div>;
    }
    return (
      <div style={{ marginTop:10, display:'flex', flexWrap:'wrap', gap:6 }}>
        {options.map(opt => {
          const sel = multi ? (Array.isArray(answered) && answered.includes(opt.v)) : answered === opt.v;
          return <span key={opt.v} style={chipStyle(sel, !sel)}>{opt.l}</span>;
        })}
      </div>
    );
  }

  // ── Number input ─────────────────────────────────────────────────────────────
  if (inputType === 'number') {
    return (
      <div style={{ marginTop:10, display:'flex', alignItems:'center', flexWrap:'wrap', gap:8 }}>
        <input type="number" min={step.min} max={step.max} value={pendingInput || ''}
          onChange={e => onInputChange(e.target.value)} placeholder={step.placeholder}
          style={{ width:90, padding:'8px 12px', borderRadius:9, border:'1px solid rgba(255,255,255,0.12)',
            background:'rgba(255,255,255,0.05)', color:'#E8EDF8',
            fontFamily:'ui-monospace,Menlo,monospace', fontSize:14, textAlign:'center' }} />
        {step.unit && <span style={{ fontFamily:'Inter,system-ui', fontSize:12, color:'rgba(232,237,248,0.45)' }}>{step.unit}</span>}
        <button onClick={onInputConfirm} disabled={!pendingInput}
          style={{ ...btnSty, background: pendingInput ? '#2563EB' : 'rgba(59,130,246,0.18)', color: pendingInput ? '#fff' : 'rgba(255,255,255,0.30)' }}>
          Confirmar
        </button>
        {skipBtn}
        {deviceNote}
      </div>
    );
  }

  // ── Body metrics (altura + peso) ─────────────────────────────────────────────
  if (inputType === 'body-metrics') {
    const hasAny = pendingInputs?.height || pendingInputs?.weight;
    return (
      <div style={{ marginTop:10 }}>
        <div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
          {[['height','Altura','cm'], ['weight','Peso','kg']].map(([field, label, unit]) => (
            <div key={field} style={{ display:'flex', alignItems:'center', gap:5 }}>
              <input type="number" min={field==='height'?100:30} max={field==='height'?250:300}
                value={pendingInputs?.[field] || ''}
                onChange={e => onBodyMetricsChange(field, e.target.value)}
                placeholder={label}
                style={{ width:80, padding:'8px 10px', borderRadius:9, border:'1px solid rgba(255,255,255,0.12)',
                  background:'rgba(255,255,255,0.05)', color:'#E8EDF8',
                  fontFamily:'ui-monospace,Menlo,monospace', fontSize:13, textAlign:'center' }} />
              <span style={{ fontFamily:'Inter,system-ui', fontSize:11, color:'rgba(232,237,248,0.40)' }}>{unit}</span>
            </div>
          ))}
        </div>
        <div style={{ display:'flex', gap:8 }}>
          <button onClick={onBodyMetricsConfirm} disabled={!hasAny}
            style={{ ...btnSty, background: hasAny ? '#2563EB' : 'rgba(59,130,246,0.18)', color: hasAny ? '#fff' : 'rgba(255,255,255,0.30)' }}>
            Confirmar
          </button>
          {skipBtn}
        </div>
        {deviceNote}
      </div>
    );
  }

  // ── Free text input ──────────────────────────────────────────────────────────
  if (inputType === 'text') {
    return (
      <div style={{ marginTop:10 }}>
        <textarea value={pendingInput || ''} onChange={e => onInputChange(e.target.value)}
          placeholder={step.placeholder || ''}
          rows={2}
          style={{ width:'100%', boxSizing:'border-box', padding:'9px 12px', borderRadius:9, marginBottom:8, resize:'none',
            border:'1px solid rgba(255,255,255,0.12)', background:'rgba(255,255,255,0.05)',
            color:'rgba(232,237,248,0.80)', fontFamily:'Inter,system-ui', fontSize:12, lineHeight:1.55 }} />
        <div style={{ display:'flex', gap:8 }}>
          <button onClick={onInputConfirm} disabled={!pendingInput?.trim()}
            style={{ ...btnSty, background: pendingInput?.trim() ? '#2563EB' : 'rgba(59,130,246,0.18)', color: pendingInput?.trim() ? '#fff' : 'rgba(255,255,255,0.30)' }}>
            Confirmar
          </button>
          {skipBtn}
        </div>
        {deviceNote}
      </div>
    );
  }

  // ── Multi-select ──────────────────────────────────────────────────────────────
  if (multi) {
    const count = pendingMulti.length;
    return (
      <div style={{ marginTop:10 }}>
        <div style={{ display:'flex', flexWrap:'wrap', gap:6, marginBottom:10 }}>
          {options.map(opt => {
            const sel     = pendingMulti.includes(opt.v);
            const isNone  = opt.v === noneOption;
            const noneOn  = noneOption && pendingMulti.includes(noneOption);
            const atMax   = !isNone && !noneOn && count >= maxSelect && !sel;
            return (
              <button key={opt.v} onClick={() => !atMax && onMultiToggle(opt.v)}
                style={{
                  padding:'6px 13px', borderRadius:8, cursor: atMax ? 'default' : 'pointer',
                  fontFamily:'Inter,system-ui', fontSize:12, fontWeight:600, transition:'all .12s',
                  background: sel ? (isNone ? 'rgba(34,197,94,0.18)' : '#2563EB') : atMax ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.07)',
                  color: sel ? (isNone ? '#4ADE80' : '#fff') : atMax ? 'rgba(232,237,248,0.22)' : 'rgba(232,237,248,0.70)',
                  border: `1px solid ${sel ? (isNone ? 'rgba(34,197,94,0.35)' : 'transparent') : 'rgba(255,255,255,0.10)'}`,
                }}>
                {sel ? '✓ ' : ''}{opt.l}
              </button>
            );
          })}
        </div>
        {hasNoteField && (
          <textarea value={pendingNote || ''} onChange={e => onNoteChange(e.target.value)}
            placeholder="Describe cualquier limitación relevante (opcional)"
            rows={2}
            style={{ width:'100%', boxSizing:'border-box', padding:'9px 12px', borderRadius:9, marginBottom:10, resize:'none',
              border:'1px solid rgba(255,255,255,0.10)', background:'rgba(255,255,255,0.04)',
              color:'rgba(232,237,248,0.70)', fontFamily:'Inter,system-ui', fontSize:12, lineHeight:1.5 }} />
        )}
        <div style={{ display:'flex', alignItems:'center', gap:0 }}>
          <button onClick={onMultiConfirm} disabled={count === 0}
            style={{ ...btnSty, background: count > 0 ? '#2563EB' : 'rgba(59,130,246,0.18)', color: count > 0 ? '#fff' : 'rgba(255,255,255,0.30)' }}>
            Confirmar{maxSelect < 8 ? ` (${count}/${maxSelect})` : ''}
          </button>
          {skipBtn}
        </div>
        {deviceNote}
      </div>
    );
  }

  // ── Single-select option buttons ──────────────────────────────────────────────
  return (
    <div style={{ marginTop:10, display:'flex', flexWrap:'wrap', gap:6 }}>
      {options.map(opt => (
        <button key={opt.v} onClick={() => onAnswer(opt.v)}
          style={{
            padding:'6px 13px', borderRadius:8, border:'1px solid rgba(255,255,255,0.12)',
            cursor:'pointer', background:'rgba(255,255,255,0.06)',
            fontFamily:'Inter,system-ui', fontSize:12, fontWeight:600,
            color:'rgba(232,237,248,0.75)', transition:'all .12s',
          }}
          onMouseEnter={e => { e.currentTarget.style.background='rgba(59,130,246,0.18)'; e.currentTarget.style.borderColor='rgba(59,130,246,0.40)'; e.currentTarget.style.color='#93C5FD'; }}
          onMouseLeave={e => { e.currentTarget.style.background='rgba(255,255,255,0.06)'; e.currentTarget.style.borderColor='rgba(255,255,255,0.12)'; e.currentTarget.style.color='rgba(232,237,248,0.75)'; }}>
          {opt.l}
        </button>
      ))}
    </div>
  );
}

function AcTypingIndicator() {
  return (
    <div style={{ display:'flex', alignItems:'flex-end', gap:8, marginBottom:20 }}>
      <div style={{ width:28, height:28, borderRadius:8, flexShrink:0, background:'rgba(59,130,246,0.12)', border:'1px solid rgba(59,130,246,0.22)', display:'flex', alignItems:'center', justifyContent:'center', fontFamily:'ui-monospace', fontSize:11, color:'#93C5FD', fontWeight:700 }}>A</div>
      <div style={{ padding:'12px 16px', borderRadius:'4px 18px 18px 18px', background:AC.card, border:`1px solid ${AC.border}`, display:'flex', gap:5, alignItems:'center' }}>
        {[0,1,2].map(i => <div key={i} style={{ width:7, height:7, borderRadius:'50%', background:'rgba(232,237,248,0.35)', animation:'pulse 1.4s infinite', animationDelay:`${i*0.22}s` }} />)}
      </div>
    </div>
  );
}

function AcRoutineCard({ session, sessionIndex, totalSessions, routineId, routineName, onSendToBuilder, onSendToPlayer }) {
  const [open, setOpen] = React.useState(true);
  const isMobile = useIsMobile();
  const label = totalSessions > 1 ? `Día ${sessionIndex + 1} de ${totalSessions}` : null;
  return (
    <div style={{ marginTop:10, borderRadius:14, overflow:'hidden', border:`1px solid ${AC.border}`, background:AC.card2, animation:'fadeIn .25s ease' }}>
      {/* Header */}
      <div onClick={() => setOpen(o=>!o)} role="button" tabIndex={0} aria-expanded={open}
        aria-label={`${session.name} — ${open ? 'contraer' : 'expandir'}`}
        onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(o=>!o); } }}
        style={{ padding:'14px 18px', display:'flex', justifyContent:'space-between', alignItems:'center', borderBottom:`1px solid ${AC.border}`, cursor:'pointer' }}>
        <div>
          <div style={{ display:'flex', alignItems:'center', gap:8 }}>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, color:AC.text, letterSpacing:-0.2 }}>{session.name}</div>
            {label && <span style={{ padding:'1px 7px', borderRadius:999, background:'rgba(59,130,246,0.12)', border:'1px solid rgba(59,130,246,0.20)', fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:'#93C5FD', letterSpacing:0.5 }}>{label}</span>}
          </div>
          <div style={{ display:'flex', gap:16, marginTop:5 }}>
            {[
              `${session.exercises.length} ejerc`,
              `${session.totalSets} series`,
              `~${session.duration} min`,
            ].map(v => (
              <span key={v} style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700, color:AC.muted, letterSpacing:0.3 }}>{v}</span>
            ))}
          </div>
        </div>
        <span style={{ color:AC.muted, fontSize:12, transform:open?'rotate(180deg)':'none', transition:'transform .2s', fontFamily:'ui-monospace,Menlo,monospace' }}>▾</span>
      </div>
      {open && (
        <>
          {/* Column headers */}
          <div style={{ display:'grid', gridTemplateColumns:'1fr 36px 56px 38px 56px', padding:'6px 18px', borderBottom:`1px solid rgba(255,255,255,0.04)` }}>
            {['EJERCICIO','×','REPS','RIR','REST'].map((h, i) => (
              <div key={h} style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:7, fontWeight:700, color:AC.muted, letterSpacing:0.8, textAlign:i>0?'center':'left' }}>{h}</div>
            ))}
          </div>
          {session.exercises.map((ex, ei) => (
            <div key={ex.id||ei} style={{ display:'grid', gridTemplateColumns:'1fr 36px 56px 38px 56px', padding:'9px 18px', alignItems:'center', borderTop:`1px solid rgba(255,255,255,0.03)` }}>
              <div style={{ minWidth:0 }}>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:12, fontWeight:600, color:AC.text, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{ex.name}</div>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:9, color:AC.muted, marginTop:2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{ex.muscles?.primary?.[0]||''}</div>
                {ex.progressionHint && !ex.plateauAlert && (
                  <div style={{ fontFamily:'Inter,system-ui', fontSize:9, marginTop:3, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', color: ex.progressionHint.includes('→') ? '#4ADE80' : 'rgba(147,197,253,0.65)' }}>
                    {ex.progressionHint}
                  </div>
                )}
                {ex.plateauAlert && (
                  <div style={{ fontFamily:'Inter,system-ui', fontSize:9, marginTop:3, color:'#F59E0B', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                    {ex.progressionHint || '⚠ plateau — intenta bajar RIR o variar ejercicio'}
                  </div>
                )}
              </div>
              <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:15, fontWeight:800, color:AC.text, textAlign:'center' }}>{ex.setsCount ?? (Array.isArray(ex.sets) ? ex.sets.length : ex.sets) ?? 3}</div>
              <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, color:AC.sub, textAlign:'center' }}>{ex.repsRange||'8-12'}</div>
              <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, color:'rgba(147,197,253,0.80)', textAlign:'center', fontWeight:700 }}>{ex.rir ?? 2}</div>
              <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, color:AC.muted, textAlign:'center' }}>{ex.rest||'90s'}</div>
            </div>
          ))}
          <div style={{ padding:'12px 18px', borderTop:'1px solid rgba(255,255,255,0.05)', display:'flex', flexDirection: isMobile ? 'column' : 'row', gap:8 }}>
            <button
              onClick={() => onSendToPlayer(session.exercises, { routineId, routineName, sessionIndex, totalSessions, sessionName: session.name })}
              style={{ order: isMobile ? 0 : 3, flex:2, minHeight: isMobile ? 46 : undefined, padding:'11px 16px', borderRadius:10, border:'none', cursor:'pointer', background:'#22C55E', color:'#fff', fontFamily:'Inter,system-ui', fontSize: isMobile ? 15 : 13, fontWeight:700, letterSpacing:-0.2, boxShadow:'0 4px 18px -4px rgba(34,197,94,0.45)' }}
            >
              ▶ Entrenar ahora
            </button>
            <button
              onClick={() => onSendToBuilder(session.exercises, { routineId, routineName, sessionIndex, totalSessions, sessionName: session.name })}
              style={{ flex:1, minHeight: isMobile ? 46 : undefined, padding:'11px 12px', borderRadius:10, border:'1px solid rgba(59,130,246,0.30)', cursor:'pointer', background:'transparent', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize: isMobile ? 14 : 12, fontWeight:700, letterSpacing:-0.2 }}
            >
              Abrir en Builder
            </button>
            <button
              title="Descargar PDF"
              onClick={() => typeof exportRoutinePDF !== 'undefined' && exportRoutinePDF({ name: routineName, sessions: [session] })}
              style={{ flexShrink:0, minHeight: isMobile ? 46 : undefined, padding:'11px 13px', borderRadius:10, border:'1px solid rgba(59,130,246,0.25)', cursor:'pointer', background:'rgba(59,130,246,0.07)', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize: isMobile ? 14 : 13, fontWeight:700 }}
            >
              ↓ Descargar PDF
            </button>
          </div>
        </>
      )}
    </div>
  );
}

function AcAnalysisCard({ content }) {
  const SEV_C = { warning:'#EF4444', good:'#22C55E', info:'#3B82F6' };
  const { stats } = content;
  const fatLabel = { high:'ALTA', moderate:'MOD', low:'BAJA' }[stats?.fatigue] || '—';
  const fatColor = { high:'#EF4444', moderate:'#F59E0B', low:'#22C55E' }[stats?.fatigue] || AC.muted;

  const metrics = [
    { k:'SESIONES', v: stats?.sessions ?? '—',                    c: AC.text },
    { k:'ESTA SEM', v: stats?.week ?? '—',                        c: AC.text },
    { k:'RACHA',    v: stats?.streak !== undefined ? `${stats.streak}d` : '—', c: (stats?.streak||0) >= 3 ? '#22C55E' : AC.text },
    stats?.push > 0
      ? { k:'P:T', v:`${stats.push}:${stats.pull}`, c: stats.push > stats.pull * 1.4 ? '#F59E0B' : AC.text }
      : null,
    { k:'FATIGA',   v: fatLabel,                                   c: fatColor },
  ].filter(Boolean);

  return (
    <div style={{ borderRadius:14, overflow:'hidden', border:`1px solid ${AC.border}`, animation:'fadeIn .25s ease' }}>

      {/* ── Metric strip — Whoop-style ── */}
      <div style={{ display:'flex', borderBottom:`1px solid ${AC.border}` }}>
        {metrics.map((m, i) => (
          <div key={m.k} style={{
            flex:1, padding:'14px 10px', textAlign:'center',
            borderRight: i < metrics.length - 1 ? `1px solid ${AC.border}` : 'none',
          }}>
            <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:7, color:AC.muted, fontWeight:700, letterSpacing:0.9, marginBottom:7 }}>{m.k}</div>
            <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:20, fontWeight:800, color:m.c, lineHeight:1 }}>{m.v}</div>
          </div>
        ))}
      </div>

      {/* ── Summary ── */}
      <div style={{ padding:'13px 18px', borderBottom:`1px solid ${AC.border}`, background:'rgba(255,255,255,0.02)' }}>
        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:AC.muted, letterSpacing:1.6, marginBottom:7 }}>ANÁLISIS</div>
        <p style={{ fontFamily:'Inter,system-ui', fontSize:13, color:AC.sub, margin:0, lineHeight:1.55 }}>{content.summary}</p>
      </div>

      {/* ── Issue rows — left accent border ── */}
      {(content.issues || []).map((issue, i) => (
        <div key={i} style={{
          padding:'12px 18px',
          borderTop: i > 0 ? `1px solid rgba(255,255,255,0.04)` : undefined,
          borderLeft:`2px solid ${SEV_C[issue.sev] || AC.blue}`,
        }}>
          <div style={{ display:'flex', alignItems:'flex-start', gap:10 }}>
            <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, color:SEV_C[issue.sev]||AC.blue, flexShrink:0, marginTop:1, fontWeight:700 }}>{issue.icon}</span>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, color:AC.text, marginBottom:3 }}>{issue.title}</div>
              <div style={{ fontFamily:'Inter,system-ui', fontSize:11, color:AC.sub, lineHeight:1.55, marginBottom:issue.rec?5:0 }}>{issue.detail}</div>
              {issue.rec && (
                <div style={{ fontFamily:'Inter,system-ui', fontSize:10, color:SEV_C[issue.sev]||AC.blue, fontWeight:600 }}>→ {issue.rec}</div>
              )}
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

// Discreet, honest "this is the basic version" notice. Sets expectations that the
// intelligent (AI) Coach is coming, without dominating the chat. Dismissable, and
// the dismissal persists.
const AC_BETA_KEY = 'atlas.coach.betaNotice.dismissed.v1';
// Discreet "Beta" pill shown next to the "ATLAS COACH" title. Tapping it
// re-opens the beta notice (even after it has been dismissed).
function AcBetaTag({ onClick }) {
  return (
    <button onClick={onClick} aria-label="Atlas Coach está en beta — ver información" title="Atlas Coach está en beta"
      style={{
        display:'inline-flex', alignItems:'center', padding:'2px 7px', borderRadius:5, cursor:'pointer',
        border:'1px solid rgba(59,130,246,0.30)', background:'rgba(59,130,246,0.10)', color:'#93C5FD',
        fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700, letterSpacing:1, textTransform:'uppercase', lineHeight:1,
      }}>
      Beta
    </button>
  );
}

// Dismissible beta notice. Controlled by AtlasCoachSection so the "Beta" tag can
// re-open it; dismissing persists so it won't auto-appear again.
function AcBetaNotice({ onClose }) {
  return (
    <div style={{
      display:'flex', alignItems:'flex-start', gap:10, marginBottom:18,
      padding:'11px 13px', borderRadius:11,
      border:'1px solid rgba(59,130,246,0.20)', background:'rgba(59,130,246,0.05)',
    }}>
      <span aria-hidden style={{ width:6, height:6, borderRadius:'50%', background:'#3B82F6', flexShrink:0, marginTop:6, boxShadow:'0 0 8px rgba(59,130,246,0.7)' }} />
      <span style={{ flex:1, minWidth:0, fontFamily:'Inter,system-ui', fontSize:12, lineHeight:1.55, color:'rgba(232,237,248,0.62)' }}>
        Atlas Coach está en fase beta. Esta versión responde con recomendaciones predefinidas; estamos trabajando en una versión con inteligencia artificial que analizará tu entrenamiento y te dará recomendaciones más precisas.
      </span>
      <button onClick={onClose} aria-label="Ocultar aviso" style={{
        flexShrink:0, width:26, height:26, borderRadius:7, border:'none', cursor:'pointer',
        background:'transparent', color:'rgba(232,237,248,0.32)', fontSize:13, lineHeight:1,
      }}>✕</button>
    </div>
  );
}

function AcAulaChips({ articleIds, onOpenAula, onOpenGlossary }) {
  if (!articleIds?.length) return null;
  // Resolve every related id to a REAL destination: a published Aula article
  // (preferred) or, failing that, an existing glossary term. Ids that match
  // neither are dropped — we never surface an internal identifier, and the
  // title/label always comes from the real content (not a hardcoded map that
  // could drift). If nothing resolves, the whole section is hidden.
  const pub = (typeof ArticlesService !== 'undefined' && ArticlesService.getAllSync)
    ? ArticlesService.getAllSync({ status: 'published' }) : [];
  const artById = new Map(pub.map(a => [a.id, a]));
  const seen = new Set();
  const chips = [];
  for (const id of articleIds) {
    if (!id || seen.has(id)) continue;
    seen.add(id);
    const art = artById.get(id);
    if (art) { chips.push({ id, label: art.title, kind: 'article' }); continue; }
    const term = (typeof GlossaryService !== 'undefined' && GlossaryService.getById)
      ? GlossaryService.getById(id) : null;
    if (term) { chips.push({ id, label: term.term, kind: 'glossary' }); continue; }
    // Unknown id (no real article or glossary entry) → skip it entirely.
  }
  if (!chips.length) return null;
  const go = (c) => c.kind === 'article'
    ? (onOpenAula && onOpenAula(c.id))
    : (onOpenGlossary && onOpenGlossary(c.id));
  return (
    <div style={{ display:'flex', flexWrap:'wrap', gap:6, marginTop:8, paddingLeft:2 }}>
      <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:'rgba(232,237,248,0.30)', letterSpacing:1, alignSelf:'center', flexShrink:0 }}>LEER EN AULA</span>
      {chips.map(c => (
        <button
          key={c.kind + ':' + c.id}
          onClick={() => go(c)}
          style={{
            padding:'4px 11px', borderRadius:999,
            border:'1px solid rgba(59,130,246,0.22)',
            background:'rgba(59,130,246,0.07)',
            color:'#93C5FD', fontFamily:'"Inter",system-ui', fontSize:11, fontWeight:600,
            cursor:'pointer', transition:'background .12s',
          }}
          onMouseEnter={e => { e.currentTarget.style.background = 'rgba(59,130,246,0.15)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'rgba(59,130,246,0.07)'; }}
        >
          {c.label} →
        </button>
      ))}
    </div>
  );
}

// Renders message text, turning an optional { label, to } link into an in-line
// anchor (keyboard-focusable, right-clickable) when the label appears verbatim
// in the text. Falls back to plain text if the label isn't present.
// Convierte **negrita** de Markdown en elementos <strong> (nodos React, nunca
// innerHTML) para que no se muestren los asteriscos literales. Devuelve un array
// de cadenas y elementos listo para renderizar.
function acRenderBold(text, keyPrefix) {
  if (typeof text !== 'string' || text.indexOf('**') === -1) return [text];
  const out = []; const re = /\*\*(.+?)\*\*/g; let last = 0, m, n = 0;
  while ((m = re.exec(text))) {
    if (m.index > last) out.push(text.slice(last, m.index));
    out.push(React.createElement('strong', { key: `${keyPrefix}-b${n++}`, style: { fontWeight: 700 } }, m[1]));
    last = m.index + m[0].length;
  }
  if (last < text.length) out.push(text.slice(last));
  return out;
}
function acRenderTextWithLink(text, link, navigate) {
  if (typeof text !== 'string') return text;
  // Con enlace: divide por su etiqueta y aplica negrita a cada tramo de texto.
  if (link && link.label && text.indexOf(link.label) !== -1) {
    const idx = text.indexOf(link.label);
    const linkStyle = { color: '#93C5FD', fontWeight: 700, textDecoration: 'underline', textUnderlineOffset: 2, cursor: 'pointer' };
    return [
      ...acRenderBold(text.slice(0, idx), 'pre'),
      React.createElement('a', {
        key: 'coach-link',
        href: '#' + link.to,
        onClick: (e) => { e.preventDefault(); navigate && navigate(link.to); },
        style: linkStyle,
      }, link.label),
      ...acRenderBold(text.slice(idx + link.label.length), 'post'),
    ];
  }
  // Sin enlace: solo negrita.
  return acRenderBold(text, 'b');
}

function AcCoachMessage({ content, onSendToBuilder, onSendToPlayer, onboardingProps, onOpenAula, onOpenGlossary }) {
  const isMobile = useIsMobile();
  const { navigate } = useRoute();
  const bubble = { padding:'13px 18px', borderRadius:'4px 18px 18px 18px', background:AC.card, border:`1px solid ${AC.border}`, fontFamily:'Inter,system-ui', fontSize: isMobile ? 15 : 14, lineHeight:1.65, color:AC.text, whiteSpace:'pre-line' };
  // Defensive: a malformed/legacy message must never crash the whole Coach
  // (a render error would blank the sidebar + conversation list via the boundary).
  if (content == null) return null;
  if (typeof content === 'string') return <div style={bubble}>{content}</div>;
  if (content.type === 'text') return (
    <div>
      <div style={bubble}>{acRenderTextWithLink(content.text, content.link, navigate)}</div>
      <AcAulaChips articleIds={content.relatedArticles} onOpenAula={onOpenAula} onOpenGlossary={onOpenGlossary} />
    </div>
  );
  if (content.type === 'routine') return (
    <div>
      <div style={bubble}>{acRenderTextWithLink(content.text, content.link, navigate)}</div>
      {(content.sessions || []).map((session, si) => (
        <AcRoutineCard
          key={si}
          session={session}
          sessionIndex={si}
          totalSessions={content.sessions.length}
          routineId={content.routineId}
          routineName={content.routineName}
          onSendToBuilder={onSendToBuilder}
          onSendToPlayer={onSendToPlayer}
        />
      ))}
      <AcAulaChips articleIds={content.relatedArticles} onOpenAula={onOpenAula} onOpenGlossary={onOpenGlossary} />
    </div>
  );
  if (content.type === 'analysis') return (
    <div>
      <AcAnalysisCard content={content} />
      <AcAulaChips articleIds={content.relatedArticles} onOpenAula={onOpenAula} onOpenGlossary={onOpenGlossary} />
    </div>
  );
  if (content.type === 'onboarding-step') {
    const ob = onboardingProps || {};
    return (
      <div style={bubble}>
        <div style={{ marginBottom:2 }}>{content.question}</div>
        <AcOnboardingStep
          step={content}
          answered={ob.answers?.[content.id]}
          pendingMulti={ob.pendingMulti || []}
          pendingInput={ob.pendingInput || ''}
          pendingInputs={ob.pendingInputs || {}}
          pendingNote={ob.pendingNote || ''}
          onAnswer={v => ob.onAnswer?.(content.id, v)}
          onMultiToggle={ob.onMultiToggle}
          onMultiConfirm={ob.onMultiConfirm}
          onInputChange={ob.onInputChange}
          onInputConfirm={ob.onInputConfirm}
          onBodyMetricsChange={ob.onBodyMetricsChange}
          onBodyMetricsConfirm={ob.onBodyMetricsConfirm}
          onNoteChange={ob.onNoteChange}
          onSkip={ob.onSkip}
        />
      </div>
    );
  }
  return <div style={bubble}>{String(content)}</div>;
}

function AcMessageBubble({ msg, onSendToBuilder, onSendToPlayer, onboardingProps, onOpenAula, onOpenGlossary }) {
  const isUser = msg.role === 'user';
  const isMobile = useIsMobile();
  // Routine/onboarding cards go full width; plain messages stay ~85% on mobile.
  const rich = !isUser && msg.content && (msg.content.type === 'routine' || msg.content.type === 'onboarding-step' || msg.content.type === 'analysis');
  const bubbleMax = rich ? '100%' : (isMobile ? '85%' : '76%');
  return (
    <div style={{ display:'flex', justifyContent:isUser?'flex-end':'flex-start', alignItems:'flex-end', gap:8, marginBottom:20, animation:'fadeIn .22s ease' }}>
      {!isUser && !rich && (
        <div style={{ width:28, height:28, borderRadius:8, flexShrink:0, background:'rgba(59,130,246,0.12)', border:'1px solid rgba(59,130,246,0.22)', display:'flex', alignItems:'center', justifyContent:'center', fontFamily:'ui-monospace', fontSize:11, color:'#93C5FD', fontWeight:700, marginBottom:18 }}>A</div>
      )}
      <div style={{ maxWidth: bubbleMax, width: rich ? '100%' : undefined, minWidth:0 }}>
        {isUser
          ? <div style={{ padding:'11px 16px', borderRadius:'18px 18px 4px 18px', background:'#2563EB', color:'#fff', fontFamily:'Inter,system-ui', fontSize: isMobile ? 15 : 14, lineHeight:1.55, wordBreak:'break-word' }}>{typeof msg.content === 'string' ? msg.content : ((msg.content && msg.content.text) || '')}</div>
          : <AcCoachMessage content={msg.content} onSendToBuilder={onSendToBuilder} onSendToPlayer={onSendToPlayer} onboardingProps={onboardingProps} onOpenAula={onOpenAula} onOpenGlossary={onOpenGlossary} />
        }
        <div style={{ fontFamily:'Inter,system-ui', fontSize:10, color:'rgba(232,237,248,0.22)', marginTop:5, textAlign:isUser?'right':'left' }}>
          {new Date(msg.ts).toLocaleTimeString('es', { hour:'2-digit', minute:'2-digit' })}
        </div>
      </div>
    </div>
  );
}

// ── Mi Rutina widget ──────────────────────────────────────────────────────────
function AcMiRutina({ onSendToBuilder, onSendToPlayer }) {
  const { state, actions } = useStore();
  const { navigate } = useRoute();
  const routine = React.useMemo(() => { try { return JSON.parse(localStorage.getItem('atlas.routine.active.v1') || 'null'); } catch { return null; } }, []);
  const isSaved = !!routine && (state.savedRoutines || []).some(r => r.id === routine.id);
  const [justSaved, setJustSaved] = React.useState(false);
  if (!routine || !Array.isArray(routine.sessions) || !routine.sessions.length) return null;
  return (
    <div style={{ padding:'10px 10px 0', borderTop:`1px solid ${AC.border}`, flexShrink:0 }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:8 }}>
        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:AC.muted, letterSpacing:1.2 }}>MI RUTINA</div>
        <div style={{ display:'flex', gap:5 }}>
          <button
            title="Guardar en Mis rutinas"
            onClick={() => { actions.saveRoutine(routine); setJustSaved(true); }}
            style={{ padding:'3px 8px', borderRadius:6, border:`1px solid rgba(34,197,94,0.28)`, background:'rgba(34,197,94,0.08)', color:'#4ADE80', fontFamily:'Inter,system-ui', fontSize:9, fontWeight:700, cursor:'pointer' }}
          >{justSaved || isSaved ? '✓ Guardada' : '＋ Guardar'}</button>
          <button
            title="Descargar rutina completa en PDF"
            onClick={() => typeof exportRoutinePDF !== 'undefined' && exportRoutinePDF(routine)}
            style={{ padding:'3px 8px', borderRadius:6, border:`1px solid rgba(59,130,246,0.22)`, background:'rgba(59,130,246,0.07)', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:9, fontWeight:700, cursor:'pointer' }}
          >↓ PDF</button>
        </div>
      </div>
      <div style={{ fontFamily:'Inter,system-ui', fontSize:11, fontWeight:700, color:AC.text, marginBottom:7, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{routine.name || 'Rutina activa'}</div>
      <div style={{ display:'flex', flexDirection:'column', gap:4, marginBottom:8 }}>
        {routine.sessions.map((session, i) => (
          <div key={i} style={{ display:'flex', gap:4 }}>
            <button
              onClick={() => onSendToBuilder(session.exercises, { routineId: routine.id, routineName: routine.name, sessionIndex: i, totalSessions: routine.sessions.length, sessionName: session.name })}
              style={{ flex:1, padding:'5px 8px', borderRadius:7, border:`1px solid rgba(59,130,246,0.22)`, background:'rgba(59,130,246,0.06)', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:10, fontWeight:700, cursor:'pointer', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', textAlign:'left' }}
            >{session.name || `Día ${i+1}`}</button>
            <button
              onClick={() => onSendToPlayer(session.exercises, { routineId: routine.id, routineName: routine.name, sessionIndex: i, totalSessions: routine.sessions.length, sessionName: session.name })}
              aria-label={`Entrenar ${session.name || `Día ${i+1}`} ahora`} title="Entrenar ahora"
              style={{ flexShrink:0, minWidth:28, minHeight:28, padding:'5px 9px', borderRadius:7, border:'none', background:'#16A34A', color:'#fff', fontFamily:'Inter,system-ui', fontSize:11, fontWeight:800, cursor:'pointer' }}
            >▶</button>
          </div>
        ))}
      </div>
      <button
        onClick={() => navigate('/rutinas')}
        style={{ width:'100%', padding:'6px', borderRadius:7, border:`1px solid ${AC.border}`, background:'transparent', color:AC.muted, fontFamily:'Inter,system-ui', fontSize:10, fontWeight:700, cursor:'pointer', marginBottom:8 }}
      >Mis rutinas →</button>
    </div>
  );
}

// ── Sidebar ───────────────────────────────────────────────────────────────────
// Formats a chat's creation date for the list ("12 jul")
function acChatDate(chat) {
  const ts = chat.createdAt || (chat.id && Number(String(chat.id).replace('chat-', ''))) || 0;
  if (!ts) return '';
  try { return new Date(ts).toLocaleDateString('es-ES', { day: 'numeric', month: 'short' }); }
  catch { return ''; }
}

// A single conversation row with a ⋯ menu → delete + confirmation.
// Delete only removes this conversation's messages; nothing else is touched.
function AcChatRow({ chat, active, onSelect, onDelete }) {
  const [menu, setMenu]       = React.useState(false);
  const [confirm, setConfirm] = React.useState(false);
  const menuBtnRef = React.useRef(null);
  const msgs = Array.isArray(chat.messages) ? chat.messages : [];
  const nMsg = msgs.filter(m => m && m.content?.type !== 'onboarding-step').length;

  return (
    <div style={{ position:'relative', marginBottom:2 }}>
      <div style={{ display:'flex', alignItems:'stretch', borderRadius:8, background: active ? 'rgba(59,130,246,0.12)' : 'transparent' }}
        onMouseEnter={e => { if (!active) e.currentTarget.style.background='rgba(255,255,255,0.04)'; }}
        onMouseLeave={e => { if (!active) e.currentTarget.style.background='transparent'; }}>
        <button onClick={() => onSelect(chat.id)} style={{ flex:1, minWidth:0, padding:'9px 6px 9px 10px', border:'none', cursor:'pointer', textAlign:'left', background:'transparent' }}>
          <div style={{ fontFamily:'Inter,system-ui', fontSize:12, fontWeight:600, color: active ? '#93C5FD' : AC.sub, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{chat.title || 'Conversación'}</div>
          <div style={{ fontFamily:'Inter,system-ui', fontSize:10, color:AC.muted, marginTop:2 }}>
            {acChatDate(chat) ? `${acChatDate(chat)} · ` : ''}{nMsg} mensaje{nMsg!==1?'s':''}
          </div>
        </button>
        <button ref={menuBtnRef} aria-label="Opciones de conversación" onClick={() => setMenu(m => !m)}
          style={{ flexShrink:0, width:34, border:'none', cursor:'pointer', background:'transparent', color:AC.muted, fontSize:16, lineHeight:1, borderRadius:8 }}>⋯</button>
      </div>

      {/* Portal menu — never clipped by the sidebar/sheet scroll container */}
      <AtlasMenu open={menu} anchorRef={menuBtnRef} onClose={() => setMenu(false)} width={180}>
        <button onClick={() => { setMenu(false); setConfirm(true); }} style={{ display:'block', width:'100%', textAlign:'left', padding:'12px 14px', border:'none', cursor:'pointer', background:'transparent', color:'#FCA5A5', fontFamily:'Inter,system-ui', fontSize:13, fontWeight:600 }}>Eliminar conversación</button>
      </AtlasMenu>

      {confirm && (
        <div style={{ margin:'6px 4px 8px', padding:'12px 14px', borderRadius:10, border:'1px solid rgba(239,68,68,0.30)', background:'rgba(239,68,68,0.06)' }}>
          <div style={{ fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, color:AC.text }}>¿Eliminar esta conversación?</div>
          <div style={{ fontFamily:'Inter,system-ui', fontSize:11, color:AC.sub, marginTop:3, lineHeight:1.5 }}>No se puede deshacer. Solo se borran los mensajes; tu historial, rutinas y perfil no cambian.</div>
          <div style={{ display:'flex', gap:8, marginTop:10 }}>
            <button onClick={() => onDelete(chat.id)} style={{ minHeight:40, padding:'0 14px', borderRadius:8, border:'none', cursor:'pointer', background:'#EF4444', color:'#fff', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700 }}>Eliminar</button>
            <button onClick={() => setConfirm(false)} style={{ minHeight:40, padding:'0 14px', borderRadius:8, cursor:'pointer', background:'transparent', color:AC.sub, border:`1px solid ${AC.border}`, fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700 }}>Cancelar</button>
          </div>
        </div>
      )}
    </div>
  );
}

// Mobile conversations sheet — slides up from the bottom, above the tab bar.
// Rendered through a portal to <body>: the route content is wrapped by
// PageTransition, whose `transform` makes it the containing block for any
// `position:fixed` descendant (iOS Safari enforces this strictly), which
// confined/hid this sheet on mobile. Portaling to <body> escapes that
// transform so `position:fixed; inset:0` is relative to the real viewport.
// Desktop uses the non-fixed <aside> sidebar, so it was never affected.
function AcMobileChatSheet({ chats, activeChatId, onSelect, onNew, onDelete, onClose }) {
  const list = Array.isArray(chats) ? chats : [];
  const sheet = (
    // zIndex 2000: above the mobile tab bar (300) so the modal fully covers,
    // below the AtlasMenu portal (3000) so the ⋯ delete menu stays on top.
    <div style={{ position:'fixed', inset:0, zIndex:2000, display:'flex', flexDirection:'column', justifyContent:'flex-end' }}>
      <div onClick={onClose} style={{ position:'absolute', inset:0, background:'rgba(0,0,0,0.5)', animation:'fadeIn .15s ease' }} />
      <div style={{ position:'relative', background:AC.page, borderTop:`1px solid ${AC.border}`, borderRadius:'18px 18px 0 0', maxHeight:'80vh', display:'flex', flexDirection:'column', paddingBottom:'calc(16px + env(safe-area-inset-bottom))', boxShadow:'0 -12px 40px rgba(0,0,0,0.45)', animation:'acSheetUp .26s cubic-bezier(0.32,0.72,0,1)' }}>
        <div style={{ width:36, height:4, borderRadius:2, background:'rgba(255,255,255,0.18)', margin:'8px auto 0', flexShrink:0 }} />
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 18px 12px', flexShrink:0 }}>
          <span style={{ fontFamily:'Inter,system-ui', fontSize:15, fontWeight:800, color:AC.text }}>Conversaciones</span>
          <button onClick={onClose} aria-label="Cerrar" style={{ width:34, height:34, borderRadius:8, border:'none', background:'rgba(255,255,255,0.06)', color:AC.sub, fontSize:15, cursor:'pointer' }}>✕</button>
        </div>
        <div style={{ padding:'0 12px 8px', flexShrink:0 }}>
          <button onClick={onNew} style={{ width:'100%', minHeight:46, borderRadius:11, border:'1px solid rgba(59,130,246,0.25)', background:'rgba(59,130,246,0.10)', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700, cursor:'pointer' }}>+ Nueva conversación</button>
        </div>
        <div style={{ flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding:'6px 8px 12px', minHeight:0 }}>
          {list.length === 0
            ? <div style={{ padding:'24px 14px', textAlign:'center', fontFamily:'Inter,system-ui', fontSize:13, color:AC.muted }}>Aún no tienes conversaciones. Pulsa “+ Nueva conversación” para empezar.</div>
            : list.map(chat => (
                <AcChatRow key={chat.id} chat={chat} active={chat.id === activeChatId} onSelect={onSelect} onDelete={onDelete} />
              ))}
        </div>
      </div>
    </div>
  );
  if (typeof ReactDOM !== 'undefined' && ReactDOM.createPortal && typeof document !== 'undefined') {
    return ReactDOM.createPortal(sheet, document.body);
  }
  return sheet;
}

function AcSidebar({ chats, activeChatId, onSelect, onNew, onDelete, onSendToBuilder, onSendToPlayer, onBetaClick }) {
  return (
    <aside style={{ width:220, flexShrink:0, background:AC.sidebar, borderRight:`1px solid ${AC.border}`, display:'flex', flexDirection:'column', overflow:'hidden' }}>
      <div style={{ padding:'14px 12px', borderBottom:`1px solid ${AC.border}`, flexShrink:0 }}>
        <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:10 }}>
          <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700, color:AC.muted, letterSpacing:1.4 }}>ATLAS COACH</span>
          {AC_BETA && <AcBetaTag onClick={onBetaClick} />}
        </div>
        <button onClick={onNew} style={{ width:'100%', padding:'9px 12px', borderRadius:9, border:'1px solid rgba(59,130,246,0.25)', background:'rgba(59,130,246,0.08)', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor:'pointer', textAlign:'left', display:'flex', alignItems:'center', gap:7, transition:'background .12s' }}
          onMouseEnter={e => e.currentTarget.style.background='rgba(59,130,246,0.16)'}
          onMouseLeave={e => e.currentTarget.style.background='rgba(59,130,246,0.08)'}>
          <span style={{ fontSize:16, lineHeight:1 }}>+</span> Nuevo chat
        </button>
      </div>
      <div style={{ flex:1, overflowY:'auto', padding:'8px 6px' }}>
        {chats.map(chat => (
          <AcChatRow key={chat.id} chat={chat} active={chat.id === activeChatId} onSelect={onSelect} onDelete={onDelete} />
        ))}
      </div>
      <AcMiRutina onSendToBuilder={onSendToBuilder} onSendToPlayer={onSendToPlayer} />
    </aside>
  );
}

// Tracks how much the on-screen keyboard overlaps the layout viewport, using the
// visualViewport API. On iOS Safari the layout viewport does NOT shrink when the
// keyboard opens, so a bottom-anchored input gets hidden; this returns the overlap
// (in px) so the composer can be lifted above the keyboard.
function useKeyboardInset(enabled) {
  const [inset, setInset] = React.useState(0);
  React.useEffect(() => {
    const vv = window.visualViewport;
    if (!enabled || !vv) { setInset(0); return; }
    const update = () => {
      const overlap = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
      setInset(overlap);
    };
    vv.addEventListener('resize', update);
    vv.addEventListener('scroll', update);
    update();
    return () => { vv.removeEventListener('resize', update); vv.removeEventListener('scroll', update); };
  }, [enabled]);
  return inset;
}

// ── Main component ────────────────────────────────────────────────────────────
function AtlasCoachSection() {
  const { state } = useStore();
  const { navigate } = useRoute();

  const [profile, setProfile] = React.useState(() => acLoadProfile());
  const [memory,  setMemory]  = React.useState(() => acLoadMemory());
  const [chats,   setChats]   = React.useState(() => {
    try { const c = JSON.parse(localStorage.getItem(AC_CHATS_KEY) || '[]'); return Array.isArray(c) ? c : []; } catch { return []; }
  });
  // Resolve the active chat SYNCHRONOUSLY at first render from the stored chats,
  // so saved conversations show their messages on the first paint. Previously
  // this was null until a post-mount effect set it, which raced with paint and
  // intermittently left a reloaded conversation blank until the user tapped it.
  const [activeChatId, setActiveChatId] = React.useState(() => {
    try { const c = JSON.parse(localStorage.getItem(AC_CHATS_KEY) || '[]'); return (Array.isArray(c) && c.length) ? c[0].id : null; } catch { return null; }
  });
  const [input,   setInput]   = React.useState('');
  const [loading, setLoading] = React.useState(false);

  // Beta notice: shown on entry until dismissed (persisted); re-openable via the
  // "Beta" tag. All of it disappears when AC_BETA is turned off.
  const [betaOpen, setBetaOpen] = React.useState(() => {
    if (!AC_BETA) return false;
    try { return localStorage.getItem(AC_BETA_KEY) !== 'true'; } catch { return true; }
  });
  const openBeta    = () => setBetaOpen(true);
  const dismissBeta = () => { try { localStorage.setItem(AC_BETA_KEY, 'true'); } catch {} setBetaOpen(false); };
  const messagesEndRef    = React.useRef(null);
  const messagesScrollRef = React.useRef(null);
  const inputRef          = React.useRef(null);
  const atBottomRef       = React.useRef(true);
  const prevScrollKeyRef  = React.useRef('');
  const [atBottom, setAtBottom] = React.useState(true);
  const [hasNew,   setHasNew]   = React.useState(false);

  // ── Atlas Profile onboarding ──────────────────────────────────────────────
  const [atlasProfile,   setAtlasProfile]   = React.useState(() => apLoadProfile());
  const [showCard,       setShowCard]       = React.useState(() => !apIsDismissed() && !apLoadProfile());
  const [showChats,      setShowChats]      = React.useState(false);
  const [onboarding,     setOnboarding]     = React.useState({ active: false, level: 1, step: 0, answers: {}, pendingMulti: [], pendingInput: '', pendingInputs: {}, pendingNote: '' });

  // ── Builder plan injection — reads plan saved by CoachRecommendationPanel ────
  const [pendingBuilderPlan] = React.useState(() => {
    try {
      if (!window.AtlasEngine) return null;
      const bp = AtlasEngine.loadBuilderPlan();
      if (!bp || !bp.volumePlan?.length) return null;
      if (Date.now() - (bp.generatedAt || 0) > 600000) return null; // expire 10 min
      AtlasEngine.clearBuilderPlan();
      return bp;
    } catch { return null; }
  });

  // Inject builder plan into active chat once activeChatId is resolved
  React.useEffect(() => {
    if (!pendingBuilderPlan || !activeChatId) return;
    const bp = pendingBuilderPlan;
    const priority = bp.volumePlan.filter(m => m.state === 'priority').map(m => m.name);
    const maintain = bp.volumePlan.filter(m => m.state === 'maintain').map(m => m.name);
    const lines = [
      'Plan del Builder recibido.',
      priority.length ? `${priority.length} grupo${priority.length > 1 ? 's' : ''} en prioridad: ${priority.slice(0, 4).join(', ')}${priority.length > 4 ? ' y más' : ''}.` : '',
      maintain.length ? `${maintain.length} en mantenimiento: ${maintain.slice(0, 3).join(', ')}${maintain.length > 3 ? ' y más' : ''}.` : '',
      `Split recomendado: ${bp.split?.name || 'Upper/Lower'} · ${bp.days?.length || 4} sesiones semanales.`,
      `Volumen objetivo: ${bp.totalSets} series/semana.`,
      '¿Genero la rutina completa con ejercicios específicos basada en este plan?',
    ].filter(Boolean);
    setChats(prev => prev.map(c => c.id !== activeChatId ? c : {
      ...c,
      messages: [...c.messages, {
        id: `bp-${Date.now()}`,
        role: 'coach',
        content: { type:'text', text: lines.join(' ') },
        ts: Date.now(),
      }],
    }));
  }, [activeChatId]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Proactive post-session analysis — flag set by logSession on save ────────
  React.useEffect(() => {
    if (!activeChatId) return;
    let pending = null;
    try { pending = JSON.parse(localStorage.getItem('atlas.coach.pendingAnalysis.v1') || 'null'); } catch { return; }
    if (!pending) return;
    localStorage.removeItem('atlas.coach.pendingAnalysis.v1');
    const liveCtx = acBuildRichContext(state, acMergeAtlasProfile(apLoadProfile(), acLoadProfile()));
    if (!liveCtx.log.length) return;
    const content = acResponseSessionAnalysis(liveCtx);
    setChats(prev => prev.map(c => c.id !== activeChatId ? c : {
      ...c,
      messages: [...c.messages, { id:`pa-${Date.now()}`, role:'coach', content, ts:Date.now() }],
    }));
  }, [activeChatId]); // eslint-disable-line react-hooks/exhaustive-deps

  // Launch the profile onboarding when sent from the Perfil section (canonical
  // configurator lives there; it hands off the questionnaire to the Coach chat).
  React.useEffect(() => {
    if (!activeChatId) return;
    let flag = null;
    try { flag = localStorage.getItem('atlas.profile.launch.v1'); } catch { return; }
    if (!flag) return;
    try { localStorage.removeItem('atlas.profile.launch.v1'); } catch {}
    setTimeout(() => startOnboarding(), 120);
  }, [activeChatId]); // eslint-disable-line react-hooks/exhaustive-deps

  const allExs = React.useMemo(() => {
    try { return ExerciseService.getAll(); } catch { return []; }
  }, []);

  // Merge atlas.profile.v1 (onboarding) with legacy coach profile — atlas.profile.v1 takes priority
  const mergedProfile = React.useMemo(() => acMergeAtlasProfile(atlasProfile, profile), [atlasProfile, profile]);

  const ctx   = React.useMemo(() => acBuildRichContext(state, mergedProfile), [state, mergedProfile]);
  const chips = React.useMemo(() => acGetDynamicChips(ctx, mergedProfile), [ctx, mergedProfile]);

  const activeChat = chats.find(c => c.id === activeChatId) || null;
  const messages   = activeChat?.messages || [];

  React.useEffect(() => {
    try {
      localStorage.setItem(AC_CHATS_KEY, JSON.stringify(chats));
    } catch (e) {
      // Quota exceeded — prune oldest messages and retry once
      try {
        const pruned = chats.map(c => ({ ...c, messages: (c.messages || []).slice(-20) })).slice(0, 5);
        localStorage.setItem(AC_CHATS_KEY, JSON.stringify(pruned));
      } catch {}
    }
  }, [chats]);

  // Scroll the MESSAGES container only (never scrollIntoView, which bubbles the
  // scroll up to <body> and drags the fixed Coach header out of view on iOS).
  const acReducedMotion = () => typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  function scrollMessagesToBottom(smooth) {
    const el = messagesScrollRef.current;
    if (!el) return;
    el.scrollTo({ top: el.scrollHeight, behavior: smooth && !acReducedMotion() ? 'smooth' : 'auto' });
  }
  function handleMessagesScroll() {
    const el = messagesScrollRef.current;
    if (!el) return;
    const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
    const bottom = dist < 80;
    atBottomRef.current = bottom;
    setAtBottom(bottom);
    if (bottom && hasNew) setHasNew(false);
  }

  // Follow new messages only when the user is already at the bottom (or just
  // sent one). If they've scrolled up, don't yank them — flag it via the button.
  React.useEffect(() => {
    const key = `${activeChatId || ''}:${messages.length}`;
    const prevChat = prevScrollKeyRef.current.split(':')[0];
    prevScrollKeyRef.current = key;
    if (prevChat !== (activeChatId || '')) {
      // Chat switched (or first load) → jump to bottom, clear unread.
      scrollMessagesToBottom(false);
      atBottomRef.current = true; setAtBottom(true); setHasNew(false);
      return;
    }
    const last = messages[messages.length - 1];
    if (atBottomRef.current || (last && last.role === 'user')) {
      scrollMessagesToBottom(true);
      setHasNew(false);
    } else {
      setHasNew(true);
    }
  }, [messages.length, activeChatId]); // eslint-disable-line react-hooks/exhaustive-deps

  // Keep the typing indicator in view while following.
  React.useEffect(() => {
    if (loading && atBottomRef.current) scrollMessagesToBottom(true);
  }, [loading]);

  // On first mount, create a fresh welcome chat only if there are none.
  // (activeChatId is already resolved synchronously from storage above.)
  React.useEffect(() => {
    if (chats.length === 0) startNewChat();
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  // Self-heal: if the active id ever points at no existing chat (e.g. after a
  // delete, or a stale id), fall back to the most recent conversation so the
  // view is never blank while chats exist.
  React.useEffect(() => {
    if (chats.length && !chats.some(c => c.id === activeChatId)) {
      setActiveChatId(chats[0].id);
    }
  }, [chats, activeChatId]);

  function startNewChat() {
    const id  = `chat-${Date.now()}`;
    const mem = acLoadMemory();
    const welcome = acWelcomeContent(state, mergedProfile, mem);
    const chat = {
      id, title:'Nueva conversación',
      messages: [{ id:`${id}-init`, role:'coach', content:welcome, ts:Date.now() }],
      createdAt: Date.now(),
    };
    setChats(prev => [chat, ...prev]);
    setActiveChatId(id);
  }

  // Delete a single conversation. Only touches atlas.chats.v1 (via the persist
  // effect) — session history, saved routines, profile and coach memory are all
  // in separate keys and are never affected.
  function deleteChat(id) {
    setChats(prev => {
      const next = prev.filter(c => c.id !== id);
      if (id === activeChatId) {
        setActiveChatId(next.length ? next[0].id : null);
        if (!next.length) {
          // recreate a fresh welcome chat so the view is never empty
          setTimeout(() => startNewChat(), 0);
        }
      }
      return next;
    });
  }

  function handleSaveProfile(p) {
    setProfile(p);
  }

  // ── Atlas Profile onboarding handlers ─────────────────────────────────────
  function _addCoachMsg(content) {
    const id = `ob-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
    setChats(prev => prev.map(c => c.id !== activeChatId ? c : {
      ...c,
      messages: [...c.messages, { id, role:'coach', content, ts:Date.now() }],
    }));
  }
  function _addUserMsg(text) {
    const id = `obu-${Date.now()}`;
    setChats(prev => prev.map(c => c.id !== activeChatId ? c : {
      ...c,
      messages: [...c.messages, { id, role:'user', content:text, ts:Date.now() }],
    }));
  }
  function _markStepAnswered(stepId, value) {
    setChats(prev => prev.map(c => c.id !== activeChatId ? c : {
      ...c,
      messages: c.messages.map(m =>
        m.content?.type === 'onboarding-step' && m.content?.id === stepId
          ? { ...m, content: { ...m.content, answered: value } }
          : m
      ),
    }));
  }

  function dismissCard() {
    apSetDismissed();
    setShowCard(false);
  }

  const _OB_RESET = { active: false, level: 1, step: 0, answers: {}, pendingMulti: [], pendingInput: '', pendingInputs: {}, pendingNote: '' };

  function startOnboarding() {
    setShowCard(false);
    setOnboarding({ ..._OB_RESET, active: true });
    _addCoachMsg({ type:'text', text:'Perfecto. Empecemos con tu Perfil Atlas. 6 preguntas rápidas.' });
    setTimeout(() => _addCoachMsg({ type:'onboarding-step', ...AP_STEPS[0] }), 400);
  }

  function editProfile() {
    setOnboarding({ ..._OB_RESET, active: true });
    _addCoachMsg({ type:'text', text:'Vamos a actualizar tu Perfil Atlas. Puedes modificar cualquier dato.' });
    setTimeout(() => _addCoachMsg({ type:'onboarding-step', ...AP_STEPS[0] }), 400);
  }

  function handleOnboardingAnswer(stepId, value) {
    // L2 offer
    if (stepId === 'l2-offer') {
      const label = AP_L2_OFFER.options.find(o => o.v === value)?.l || value;
      _markStepAnswered(stepId, value);
      _addUserMsg(label);
      if (value === 'yes') {
        setOnboarding(prev => ({ ...prev, level: 2, step: 0, pendingMulti: [], pendingInput: '', pendingInputs: {}, pendingNote: '' }));
        setTimeout(() => _addCoachMsg({ type:'onboarding-step', ...AP_STEPS_L2[0] }), 350);
      } else {
        _finishOnboarding(onboarding.answers);
      }
      return;
    }

    const steps   = onboarding.level === 1 ? AP_STEPS : AP_STEPS_L2;
    const stepIdx = onboarding.step;
    const step    = steps[stepIdx];
    if (!step || step.id !== stepId) return;

    const label = step.options?.find(o => o.v === value)?.l || String(value);
    _markStepAnswered(stepId, value);
    _addUserMsg(label);
    const newAnswers = { ...onboarding.answers, [stepId]: value };
    _advanceOnboarding(stepIdx, newAnswers, onboarding.level);
  }

  function handleMultiToggle(v) {
    setOnboarding(prev => {
      const steps   = prev.level === 1 ? AP_STEPS : AP_STEPS_L2;
      const step    = steps[prev.step];
      const maxSel  = step?.maxSelect || 3;
      const none    = step?.noneOption;
      let cur       = prev.pendingMulti;
      if (none && v === none) {
        cur = cur.includes(none) ? [] : [none];
      } else if (none && cur.includes(none)) {
        cur = [v];
      } else {
        cur = cur.includes(v) ? cur.filter(x => x !== v) : cur.length < maxSel ? [...cur, v] : cur;
      }
      return { ...prev, pendingMulti: cur };
    });
  }

  function handleMultiConfirm() {
    const steps   = onboarding.level === 1 ? AP_STEPS : AP_STEPS_L2;
    const stepIdx = onboarding.step;
    const step    = steps[stepIdx];
    if (!step || !step.multi) return;
    const values  = onboarding.pendingMulti;
    if (!values.length) return;
    const note    = onboarding.pendingNote?.trim() || null;
    const labels  = values.map(v => step.options.find(o => o.v === v)?.l || v).join(', ');
    _markStepAnswered(step.id, values);
    _addUserMsg(labels + (note ? ` — ${note}` : ''));
    const newAnswers = { ...onboarding.answers, [step.id]: values };
    if (step.hasNoteField && note) newAnswers.injuryNotes = note;
    _advanceOnboarding(stepIdx, newAnswers, onboarding.level);
  }

  function handleInputChange(value) {
    setOnboarding(prev => ({ ...prev, pendingInput: value }));
  }

  function handleInputConfirm() {
    const steps   = onboarding.level === 1 ? AP_STEPS : AP_STEPS_L2;
    const step    = steps[onboarding.step];
    if (!step || (step.inputType !== 'number' && step.inputType !== 'text')) return;
    const val = onboarding.pendingInput;
    if (!val || (typeof val === 'string' && !val.trim())) return;
    const stored  = step.inputType === 'text' ? val.trim() : Number(val);
    _markStepAnswered(step.id, val);
    _addUserMsg(step.inputType === 'text' ? val.trim() : val + (step.unit ? ' ' + step.unit : ''));
    const newAnswers = { ...onboarding.answers, [step.id]: stored };
    _advanceOnboarding(onboarding.step, newAnswers, onboarding.level);
  }

  function handleBodyMetricsChange(field, value) {
    setOnboarding(prev => ({ ...prev, pendingInputs: { ...prev.pendingInputs, [field]: value } }));
  }

  function handleBodyMetricsConfirm() {
    const steps = onboarding.level === 1 ? AP_STEPS : AP_STEPS_L2;
    const step  = steps[onboarding.step];
    if (!step || step.inputType !== 'body-metrics') return;
    const val   = onboarding.pendingInputs;
    const parts = [val.height && `${val.height} cm`, val.weight && `${val.weight} kg`].filter(Boolean);
    _markStepAnswered(step.id, val);
    _addUserMsg(parts.join(' · ') || 'Sin datos');
    const newAnswers = { ...onboarding.answers, [step.id]: val };
    _advanceOnboarding(onboarding.step, newAnswers, onboarding.level);
  }

  function handleNoteChange(value) {
    setOnboarding(prev => ({ ...prev, pendingNote: value }));
  }

  function handleSkip() {
    const steps = onboarding.level === 1 ? AP_STEPS : AP_STEPS_L2;
    const step  = steps[onboarding.step];
    if (!step) return;
    _markStepAnswered(step.id, null);
    _addUserMsg('Saltar');
    _advanceOnboarding(onboarding.step, { ...onboarding.answers }, onboarding.level);
  }

  function _advanceOnboarding(stepIdx, newAnswers, level) {
    const steps   = level === 1 ? AP_STEPS : AP_STEPS_L2;
    const nextIdx = stepIdx + 1;
    const reset   = { pendingMulti: [], pendingInput: '', pendingInputs: {}, pendingNote: '' };

    if (nextIdx < steps.length) {
      setOnboarding(prev => ({ ...prev, step: nextIdx, answers: newAnswers, ...reset }));
      setTimeout(() => _addCoachMsg({ type:'onboarding-step', ...steps[nextIdx] }), 350);
    } else if (level === 1) {
      // L1 complete — offer L2
      setOnboarding(prev => ({ ...prev, step: -1, answers: newAnswers, ...reset }));
      setTimeout(() => {
        _addCoachMsg({ type:'text', text:'Nivel 1 completado. Tus datos básicos están guardados.' });
        setTimeout(() => _addCoachMsg({ type:'onboarding-step', ...AP_L2_OFFER }), 380);
      }, 350);
    } else {
      _finishOnboarding(newAnswers);
    }
  }

  function _finishOnboarding(answers) {
    const bm = answers.bodyMetrics || {};
    const saved = {
      objective:        answers.objective        || null,
      experience:       answers.experience       || null,
      trainingDays:     answers.trainingDays     || null,
      sessionDuration:  answers.sessionDuration  || null,
      equipment:        answers.equipment        || null,
      musclePriorities: answers.musclePriorities || [],
      sex:              answers.sex              || null,
      age:              answers.age              ? Number(answers.age) : null,
      height:           bm.height               ? Number(bm.height)   : null,
      weight:           bm.weight               ? Number(bm.weight)   : null,
      injuries:          answers.injuries          || [],
      injuryNotes:       answers.injuryNotes       || null,
      previousInjuries:  answers.previousInjuries  || [],
      avoidExercises:    answers.avoidExercises     || null,
      activityLevel:     answers.activityLevel      || null,
      mainObstacle:      answers.mainObstacle       || null,
    };
    apSaveProfile(saved);
    setAtlasProfile(saved);
    setOnboarding(_OB_RESET);

    // Step 1: Show "generating" message
    setTimeout(() => {
      _addCoachMsg({ type:'text', text:'✓ Perfil guardado.\n\nAtlas está creando tu planificación personalizada...' });
    }, 350);

    // Step 2: Generate routine and open Builder
    setTimeout(() => {
      const goal   = AP_OBJ_MAP[saved.objective]  || 'hipertrofia';
      const level  = AP_EXP_MAP[saved.experience] || 'intermedio';
      const days   = saved.trainingDays            || 3;
      const tiempo = saved.sessionDuration         || 60;

      // Split selection — same logic as acResponseRoutine
      const musclePriorities = saved.musclePriorities || [];
      let splitKey = AP_DAYS_SPLIT[days] || (days >= 4 ? 'upper_lower' : days >= 3 ? 'ppl' : 'fullbody');
      // Principiante con ≤3 días → Full Body
      if ((saved.experience === 'beginner') && days <= 3) splitKey = 'fullbody';

      // Injury caps
      const injuries = (saved.injuries || []).filter(i => i !== 'none');
      const groupCaps = {};
      injuries.forEach(inj => {
        const caps = AP_INJURY_GROUP_CAPS[inj] || {};
        Object.entries(caps).forEach(([g, n]) => {
          groupCaps[g] = Math.min(groupCaps[g] ?? 99, n);
        });
      });

      // Avoid keywords
      const avoidKeywords = (saved.avoidExercises || '').toLowerCase()
        .split(/[,;]/).map(s => s.trim()).filter(Boolean);

      let sessions = [];
      try {
        sessions = acBuildDetailedRoutine(splitKey, goal, level, tiempo, allExs, groupCaps, avoidKeywords, [], null, state.log || []);
      } catch (e) {}

      if (!sessions.length || sessions.every(s => !s.exercises.length)) {
        _addCoachMsg({ type:'text', text:'No pude generar la rutina automáticamente. Escríbeme "crea mi rutina" para generarla.' });
        return;
      }

      // Save as AtlasRoutine
      const routineId = `routine-ob-${Date.now()}`;
      const splitLabel = { fullbody:'Full Body', ppl:'Push Pull Legs', upper_lower:'Upper Lower', push:'Push', pull:'Pull', legs:'Legs' }[splitKey] || splitKey.toUpperCase();
      const routineName = `${splitLabel} · ${goal}`;
      const atlasRoutine = {
        id: routineId, name: routineName, objective: goal, frequency: days,
        split: splitKey, sessions, priorities: musclePriorities,
        createdAt: Date.now(), source: 'onboarding',
      };
      arSave(atlasRoutine);

      // Store Day 1 as pending workout for Builder
      const firstSession = sessions[0];
      if (firstSession?.exercises?.length) {
        localStorage.setItem('atlas.pendingWorkout', JSON.stringify(firstSession.exercises));
        localStorage.setItem(AR_META_KEY, JSON.stringify({
          routineId, routineName,
          sessionIndex: 0,
          totalSessions: sessions.length,
          sessionName: firstSession.name,
        }));
      }

      navigate('/builder');
    }, 1800);
  }

  function sendMessage() {
    if (!input.trim() || loading || !activeChatId) return;
    const userText = input.trim();
    const chatId   = activeChatId;
    setInput('');

    const userMsg = { id:`msg-${Date.now()}`, role:'user', content:userText, ts:Date.now() };
    setChats(prev => prev.map(c => {
      if (c.id !== chatId) return c;
      const isFirst = c.messages.filter(m => m.role==='user').length === 0;
      return { ...c, title: isFirst ? userText.slice(0,42) : c.title, messages:[...c.messages, userMsg] };
    }));

    setLoading(true);
    const currentMemory = acLoadMemory();
    // Conversational memory carried on the chat (e.g. "we just asked the split").
    const pending = activeChat?.pendingIntent || null;
    // (2) nivel/objetivo/días que el usuario ya reveló en esta conversación.
    const convo = activeChat?.convoParams || null;
    setTimeout(() => {
      let response;
      try {
        response = acGenerateSmartResponse(userText, state, mergedProfile, currentMemory, allExs, pending, convo);
      } catch (err) {
        response = { type:'text', text:'Hubo un error al procesar tu mensaje. Por favor intenta de nuevo.' };
      }
      if (response._memoryUpdated) setMemory(acLoadMemory());
      // Carry conversational memory forward: remember what we just asked so the
      // user's next (often short) reply is read as the answer. awaitContext is the
      // general form; awaitSplit is kept for backward compatibility.
      const nextPending = (response && response.awaitContext)
        ? response.awaitContext
        : (response && response.awaitSplit ? 'routine-split' : null);
      const coachMsg = { id:`msg-${Date.now()}-c`, role:'coach', content:response, ts:Date.now() };
      const nextConvo = (response && response._convo) ? response._convo : (activeChat?.convoParams || null);
      setChats(prev => prev.map(c => c.id === chatId ? { ...c, pendingIntent: nextPending, convoParams: nextConvo, messages:[...c.messages, coachMsg] } : c));
      setLoading(false);
    }, 500 + Math.random() * 400);
  }

  function sendToBuilder(exercises, meta) {
    localStorage.setItem('atlas.pendingWorkout', JSON.stringify(exercises));
    if (meta) {
      try { localStorage.setItem(AR_META_KEY, JSON.stringify(meta)); } catch {}
    }
    navigate('/builder');
  }

  function sendToPlayer(exercises, meta) {
    localStorage.setItem('atlas.pendingWorkout', JSON.stringify(exercises));
    if (meta) {
      try { localStorage.setItem(AR_META_KEY, JSON.stringify({ ...meta, mode: 'player' })); } catch {}
    }
    navigate('/player');
  }

  function handleKeyDown(e) {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
  }

  const isMobile = useIsMobile();
  const kbInset  = useKeyboardInset(isMobile);
  const TABBAR   = 'calc(56px + env(safe-area-inset-bottom))';
  const [mHeaderH, setMHeaderH] = React.useState(0);

  // Mobile: chat owns the space between the app header and tab bar; drop the
  // global body bottom-padding so the section can size itself precisely, and
  // lock body scroll (see .atlas-chat) so the Coach header can't be scrolled
  // out of view by the document.
  React.useEffect(() => {
    if (!isMobile) return;
    document.body.classList.add('atlas-chat');
    return () => document.body.classList.remove('atlas-chat');
  }, [isMobile]);

  // Measure the real sticky app-header height (env(safe-area-inset-top) + bar)
  // so the section fills EXACTLY the space below it. A hardcoded offset ignored
  // the notch inset, making the section too tall → the body scrolled and the
  // Coach header disappeared. Re-measure on viewport/toolbar changes.
  React.useLayoutEffect(() => {
    if (!isMobile) { setMHeaderH(0); return; }
    const measure = () => {
      const el = document.querySelector('.atlas-mobile-header');
      setMHeaderH(el ? Math.round(el.getBoundingClientRect().height) : 0);
    };
    measure();
    window.addEventListener('resize', measure);
    window.visualViewport && window.visualViewport.addEventListener('resize', measure);
    return () => {
      window.removeEventListener('resize', measure);
      window.visualViewport && window.visualViewport.removeEventListener('resize', measure);
    };
  }, [isMobile]);

  // Keep the newest message visible when the keyboard opens (container only).
  React.useEffect(() => {
    if (kbInset > 0 && atBottomRef.current) scrollMessagesToBottom(false);
  }, [kbInset]); // eslint-disable-line react-hooks/exhaustive-deps

  const sectionH = isMobile
    ? (mHeaderH ? `calc(100dvh - ${mHeaderH}px)` : 'calc(100dvh - env(safe-area-inset-top) - 56px)')
    : 'calc(100vh - 57px)';

  return (
    <section style={{ height: sectionH, display:'flex', flexDirection:'column', background:AC.page, overflow:'hidden' }}>
      <h1 className="sr-only">Atlas Coach</h1>
      <div style={{ flex:1, display:'flex', overflow:'hidden', minHeight:0 }}>

        {!isMobile && (
          <AcSidebar
            chats={chats}
            activeChatId={activeChatId}
            onSelect={setActiveChatId}
            onNew={startNewChat}
            onDelete={deleteChat}
            onSendToBuilder={sendToBuilder}
            onSendToPlayer={sendToPlayer}
            onBetaClick={openBeta}
          />
        )}

        <div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minWidth:0 }}>

          {isMobile && (
            <div style={{ padding:'10px 16px', borderBottom:`1px solid ${AC.border}`, display:'flex', alignItems:'center', gap:10, flexShrink:0 }}>
              <span style={{ fontFamily:'ui-monospace', fontSize:10, fontWeight:700, color:AC.muted, letterSpacing:1.4 }}>ATLAS COACH</span>
              {AC_BETA && <AcBetaTag onClick={openBeta} />}
              <button onClick={() => setShowChats(true)} style={{ marginLeft:'auto', padding:'6px 12px', minHeight:34, borderRadius:8, border:`1px solid ${AC.border}`, background:'transparent', color:AC.sub, fontFamily:'Inter,system-ui', fontSize:11, fontWeight:700, cursor:'pointer' }}>Conversaciones</button>
              <button onClick={startNewChat} style={{ padding:'6px 12px', minHeight:34, borderRadius:8, border:`1px solid rgba(59,130,246,0.25)`, background:'rgba(59,130,246,0.08)', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:11, fontWeight:700, cursor:'pointer' }}>+ Nuevo</button>
            </div>
          )}

          {/* Mobile conversations sheet */}
          {isMobile && showChats && (
            <AcMobileChatSheet
              chats={chats}
              activeChatId={activeChatId}
              onSelect={(id) => { setActiveChatId(id); setShowChats(false); }}
              onNew={() => { startNewChat(); setShowChats(false); }}
              onDelete={deleteChat}
              onClose={() => setShowChats(false)}
            />
          )}

          {/* Messages + "jump to latest" affordance */}
          <div style={{ flex:1, position:'relative', display:'flex', minHeight:0 }}>
          <div ref={messagesScrollRef} onScroll={handleMessagesScroll} className="ac-messages" style={{ flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding: isMobile ? '20px 16px' : '28px 32px', minHeight:0 }}>
            <div style={{ maxWidth:700, margin:'0 auto' }}>
              {AC_BETA && betaOpen && <AcBetaNotice onClose={dismissBeta} />}
              {/* Single profile touchpoint: status chips when complete, otherwise
                  one discreet nudge to the canonical Perfil section. No sidebar
                  panel, no big card, no greeting nag — only ever one element. */}
              {!onboarding.active && (atlasProfile
                ? <AtlasProfileStatus profile={atlasProfile} onEdit={editProfile} />
                : <AcProfileNudge onGoProfile={() => navigate('/perfil')} />
              )}
              {messages.map(msg => (
                <AcMessageBubble
                  key={msg.id}
                  msg={msg}
                  onSendToBuilder={sendToBuilder}
                  onSendToPlayer={sendToPlayer}
                  onOpenAula={(id) => { localStorage.setItem('atlas.aula.pending.v1', id); navigate('/aula'); }}
                  onOpenGlossary={(id) => { localStorage.setItem('atlas.aula.glossary.pending.v1', id); navigate('/aula'); }}
                  onboardingProps={onboarding.active ? {
                    answers:              onboarding.answers,
                    pendingMulti:         onboarding.pendingMulti,
                    pendingInput:         onboarding.pendingInput,
                    pendingInputs:        onboarding.pendingInputs,
                    pendingNote:          onboarding.pendingNote,
                    onAnswer:             handleOnboardingAnswer,
                    onMultiToggle:        handleMultiToggle,
                    onMultiConfirm:       handleMultiConfirm,
                    onInputChange:        handleInputChange,
                    onInputConfirm:       handleInputConfirm,
                    onBodyMetricsChange:  handleBodyMetricsChange,
                    onBodyMetricsConfirm: handleBodyMetricsConfirm,
                    onNoteChange:         handleNoteChange,
                    onSkip:               handleSkip,
                  } : null}
                />
              ))}
              {loading && <AcTypingIndicator />}
              <div ref={messagesEndRef} />
            </div>
          </div>
          {!atBottom && messages.length > 0 && (
            <button
              onClick={() => { scrollMessagesToBottom(true); setHasNew(false); }}
              aria-label={hasNew ? 'Hay mensajes nuevos — bajar al último' : 'Bajar al último mensaje'}
              className="wp-press ac-scrolldown"
              style={{ position:'absolute', right:16, bottom:16, width:44, height:44, borderRadius:999, border:`1px solid ${AC.border}`, background:AC.card, color:AC.text, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', boxShadow:'0 6px 20px rgba(0,0,0,0.38)', zIndex:5 }}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>
              {hasNew && <span style={{ position:'absolute', top:-3, right:-3, width:13, height:13, borderRadius:999, background:'#3B82F6', border:`2px solid ${AC.page}` }} />}
            </button>
          )}
          </div>

          {/* Input — lifts above the keyboard (visualViewport) and the tab bar */}
          <div style={{ padding: isMobile ? '12px 14px' : '14px 28px',
            paddingBottom: isMobile ? `max(${TABBAR}, ${kbInset}px)` : undefined,
            transition: 'padding-bottom .15s ease',
            borderTop:`1px solid ${AC.border}`, background:AC.input, flexShrink:0 }}>
            <div style={{ maxWidth:700, margin:'0 auto' }}>
              <div style={{ display:'flex', gap:6, flexWrap:'wrap', marginBottom:10 }}>
                {chips.map(chip => (
                  <button key={chip} onClick={() => { setInput(chip); inputRef.current?.focus(); }}
                    style={{ padding:'4px 11px', borderRadius:999, border:`1px solid ${AC.border}`, background:'transparent', color:AC.muted, fontFamily:'Inter,system-ui', fontSize:11, cursor:'pointer', transition:'all .12s' }}
                    onMouseEnter={e => { e.currentTarget.style.borderColor='rgba(59,130,246,0.4)'; e.currentTarget.style.color='#93C5FD'; }}
                    onMouseLeave={e => { e.currentTarget.style.borderColor=AC.border; e.currentTarget.style.color=AC.muted; }}>
                    {chip}
                  </button>
                ))}
              </div>
              <div style={{ display:'flex', gap:10, alignItems:'flex-end' }}>
                <textarea ref={inputRef} value={input} onChange={e => setInput(e.target.value)} onKeyDown={handleKeyDown}
                  placeholder="Pregúntame cualquier cosa…" aria-label="Escribe tu mensaje a Atlas Coach" rows={1}
                  style={{ flex:1, padding: isMobile ? '13px 15px' : '11px 15px', borderRadius:14, border:`1px solid ${AC.border}`, background:AC.card, color:AC.text, fontFamily:'Inter,system-ui', fontSize: isMobile ? 16 : 14, resize:'none', lineHeight:1.5, maxHeight:100, overflowY:'auto', boxSizing:'border-box' }} />
                <button onClick={sendMessage} disabled={!input.trim() || loading}
                  style={{ padding: isMobile ? '0 18px' : '11px 18px', minHeight: isMobile ? 48 : undefined, borderRadius:12, border:'none', cursor: input.trim()&&!loading ? 'pointer' : 'default', background: input.trim()&&!loading ? AC.blue : 'rgba(59,130,246,0.18)', color: input.trim()&&!loading ? '#fff' : 'rgba(255,255,255,0.28)', fontFamily:'Inter,system-ui', fontSize: isMobile ? 15 : 13, fontWeight:700, transition:'all .15s', flexShrink:0 }}>
                  Enviar
                </button>
              </div>
              {!isMobile && (
                <div style={{ fontFamily:'Inter,system-ui', fontSize:10, color:'rgba(232,237,248,0.18)', marginTop:7, textAlign:'center' }}>
                  Enter para enviar · Shift+Enter para nueva línea
                </div>
              )}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// Expose AtlasProfile for other sections to read
const AtlasProfile = { get: apLoadProfile, save: apSaveProfile };
Object.assign(window, { AtlasCoachSection, AtlasProfile });
