/* utils-plan-parser.jsx — convierte el Markdown que devuelve el LLM en una
 * estructura tipada para el calendario de planes.
 *
 * Es tolerante porque el LLM no siempre cumple el formato pedido:
 *
 *   - Encabezados de semana pueden ser `## Semana 1`, `### Week 1`, `**Semana 1**`,
 *     `Semana 1:` o incluso solo `Semana 1` sin marcado.
 *   - Días pueden ir como `- Lun:`, `**Lun**:`, `Lunes -`, `Mon —`, etc.
 *   - Las sesiones pueden venir en una sola línea o desglosadas con bullets anidados.
 *
 * Si el markdown no matchea nada parseable, devolvemos `null` y la UI cae a
 * mostrar el texto crudo con `<MarkdownRender>` (ya implementado en screen-agents).
 *
 * Output:
 *   {
 *     title: string,
 *     summary: string,        // párrafo final largo (narrativa "por qué este plan")
 *     weeks: [{
 *       n: number,
 *       label: string,
 *       days: [{
 *         dow: 0..6 (lun=0…dom=6) | -1 si no se pudo determinar,
 *         label: "Lun" | "Mar" | …,
 *         sessions: [{
 *           name: string,
 *           intensity: string | null,      // "Z2", "Z3-Z4", "RPE 4", …
 *           duration_min: number | null,
 *           load: number | null,
 *           notes: string | null,          // resto de la línea/bullets nested
 *           tone: "rest"|"easy"|"long"|"hard"|"strength",
 *         }],
 *       }],
 *     }],
 *   }
 */

const _DOW_TABLE = (() => {
  // Multiples idiomas y abreviaturas. La clave es la forma normalizada.
  const map = {};
  const add = (idx, label, aliases) => {
    aliases.forEach((a) => {
      map[a.toLowerCase()] = { idx, label };
    });
  };
  add(0, "Lun", ["lun", "lunes", "mon", "monday", "l"]);
  add(1, "Mar", ["mar", "martes", "tue", "tues", "tuesday", "m"]);
  add(2, "Mié", ["mié", "mie", "miercoles", "miércoles", "wed", "wednesday", "x"]);
  add(3, "Jue", ["jue", "jueves", "thu", "thur", "thurs", "thursday", "j"]);
  add(4, "Vie", ["vie", "viernes", "fri", "friday", "v"]);
  add(5, "Sáb", ["sab", "sáb", "sabado", "sábado", "sat", "saturday", "s"]);
  add(6, "Dom", ["dom", "domingo", "sun", "sunday", "d"]);
  return map;
})();

const _DOW_LABELS = ["Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Dom"];

/* Inferencia de "tono" para colorear el calendario. Heurísticas en orden de
 * prioridad: descanso > fuerza > duro/intervalos > largo > suave. */
function inferTone(text) {
  const t = (text || "").toLowerCase();
  if (!t.trim()) return "rest";
  if (/(descanso|rest day|rest|off day|libre|movilidad|recuperaci(o|ó)n activa)/.test(t)) return "rest";
  if (/(fuerza|strength|gym|pesas|core|squat|deadlift|sentadilla|peso muerto)/.test(t)) return "strength";
  if (/(intervalos|intervals|tempo|threshold|umbral|series|cuestas|hill repeats|fartlek|vo2|sprint|test|carrera de|race)/.test(t)) return "hard";
  if (/\b(z[34]\b|z3-?z?4|z4|zona\s*[34])\b/.test(t)) return "hard";
  if (/(largo|long run|long ride|tirada larga)/.test(t)) return "long";
  if (/(easy|suave|recovery|z2|zona\s*2|endurance|rodaje|base)/.test(t)) return "easy";
  return "easy"; // default seguro: suave
}

/* "60 minutos" / "60 min" / "1h30" / "1:30" / "90'" → minutos.
 * Devuelve la primera duración encontrada (la "principal" — el LLM suele
 * escribir la duración total al inicio de la sesión y los desgloses después).
 */
function parseDurationMinutes(text) {
  if (!text) return null;
  // Una sesión real nunca pasa de ~10h. Si el regex saca un valor mayor,
  // casi seguro estamos parseando un total semanal o un mismatch (clock time,
  // sumas erradas, etc.) — devolver null en vez de mostrar "59h51".
  const MAX_REASONABLE_MIN = 600;
  const guard = (v) => (v != null && v > 0 && v <= MAX_REASONABLE_MIN ? v : null);

  const s = String(text).toLowerCase();
  // 1h30, 1h 30, 1h30min, 1 hora 30 — captura horas+minutos.
  let m = s.match(/(\d+)\s*h(?:oras?)?\s*(\d+)\s*(?:m|min|'|´)/);
  if (m) {
    const h = parseInt(m[1], 10);
    const mm = parseInt(m[2], 10);
    if (mm < 60) return guard(h * 60 + mm);
  }
  // 1h45 (sin sufijo de minutos), 1h
  m = s.match(/(\d+)\s*h(?:oras?)?\s*(\d+)?\b/);
  if (m) {
    const h = parseInt(m[1], 10);
    const mm = m[2] ? parseInt(m[2], 10) : 0;
    if (mm < 60) return guard(h * 60 + mm);
  }
  // 1:30 — sólo si parece duración (h <= 10 y mm < 60). Hora del día como
  // "06:00 - 07:30" se descarta acá.
  m = s.match(/\b(\d+):(\d{2})\b/);
  if (m) {
    const h = parseInt(m[1], 10);
    const mm = parseInt(m[2], 10);
    if (h <= 10 && mm < 60) return guard(h * 60 + mm);
  }
  // 60' / 60′ / 60´ / 90 min / 90 minutos
  m = s.match(/(\d+)\s*(?:['′´]|min(?:ut(?:os|es)?)?\b)/);
  if (m) return guard(parseInt(m[1], 10));
  return null;
}

/* "Z2" / "Z3-Z4" / "zona 2" / "RPE 4" / "FC 140" — la primera coincidencia. */
function parseIntensity(text) {
  if (!text) return null;
  const t = String(text);
  let m = t.match(/\bZ\s*\d(?:\s*[-–]\s*Z?\s*\d)?\b/i);
  if (m) return m[0].replace(/\s+/g, "").toUpperCase();
  m = t.match(/\bzona\s*\d(?:\s*[-–]\s*\d)?\b/i);
  if (m) return m[0].replace(/zona\s*/i, "Z").replace(/\s+/g, "");
  m = t.match(/\bRPE\s*\d+(?:\s*[-–]\s*\d+)?\b/i);
  if (m) return m[0].replace(/\s+/g, " ").toUpperCase();
  return null;
}

/* "carga 90" / "load 110" / "TSS 75" → número. */
function parseLoad(text) {
  if (!text) return null;
  const m = String(text).match(/\b(?:carga|load|tss)\s*[:=]?\s*(\d{2,3})\b/i);
  return m ? parseInt(m[1], 10) : null;
}

/* Heurística: identificar el deporte principal de una sesión por palabras
 * clave. El LLM tiende a empezar la línea con el deporte ("Carrera · 60' Z2"
 * o "Bici · 75' Z2"). Devuelve un id corto o null. */
function _inferSport(text) {
  if (!text) return null;
  const t = String(text).toLowerCase();
  if (/\bcarrera|running|trote|run\b/.test(t)) return "running";
  if (/\bbici|bike|cycling|ciclismo|spinning\b/.test(t)) return "cycling";
  if (/\bnataci|swim|piscina\b/.test(t)) return "swimming";
  if (/\btrail\b/.test(t)) return "trail_running";
  if (/\bgym|fuerza|pesas|strength|press\b/.test(t)) return "strength";
  if (/\brem(o|ar)|rowing\b/.test(t)) return "rowing";
  if (/\byoga|movilidad|stretch\b/.test(t)) return "yoga";
  if (/\btrekking|hiking|caminata\b/.test(t)) return "hiking";
  if (/\btriatl[oó]n|triathlon\b/.test(t)) return "triathlon";
  return null;
}

/* Detecta si una línea empieza con un día de la semana y devuelve
 * `{ dow, label, rest }` con el resto de la línea. Si no, null. */
function _matchDayLine(rawLine) {
  // Quitar bullets, números, asteriscos al inicio.
  let line = rawLine
    .replace(/^\s*[-*+•]\s*/, "")
    .replace(/^\s*\d+\.\s*/, "")
    .replace(/^\s*\*\*([^*]+)\*\*/, "$1") // **Lun**: …
    .replace(/^\s*__([^_]+)__/, "$1");
  line = line.trim();
  if (!line) return null;
  // Tomar la primera "palabra" delimitada por : / – / — / -.
  const m = line.match(/^([A-Za-zÁÉÍÓÚáéíóúñÑ]+)\.?\s*[:\-–—]\s*(.*)$/);
  if (!m) return null;
  const dayKey = m[1].toLowerCase().replace(/\.$/, "");
  const entry = _DOW_TABLE[dayKey];
  if (!entry) return null;
  return { dow: entry.idx, label: entry.label, rest: (m[2] || "").trim() };
}

/* Detecta encabezado de semana. Devuelve número de semana o null. */
function _matchWeekHeading(rawLine) {
  const line = rawLine
    .replace(/^\s*#+\s*/, "")           // ## …
    .replace(/^\s*\*\*(.+?)\*\*\s*$/, "$1") // **…**
    .replace(/^\s*__(.+?)__\s*$/, "$1")
    .trim();
  // "Semana 1", "Semana 1:", "Semana 1 — base", "Week 1", "Week 1 - base".
  const m = line.match(/^(?:semana|week)\s*[#nº]?\s*(\d+)\b/i);
  if (m) return { n: parseInt(m[1], 10), label: line };
  return null;
}

/* Extrae nombre de sesión + estructura del resto de la línea.
 *
 *   "Z2 endurance · 60' zona 2 · carga 70"
 *   "Tempo 30' · 10' E + 30' tempo + 10' E · carga 95"
 *   "Largo Z2 — 1h45' zona 2"
 */
function _parseSession(rest) {
  if (!rest) return null;
  // Cuts robustos: ` · `, ` — `, ` -- `, ` | `. Si no hay separadores, name = rest.
  const parts = rest.split(/\s+[·•|–—]\s+|\s{2,}--\s+/);
  const name = (parts[0] || rest).trim();
  const tail = parts.slice(1).join(" · ");
  const all = rest;
  const tone = inferTone(`${name} ${tail}`);
  const intensity = parseIntensity(all);
  const duration_min = parseDurationMinutes(all);
  const load = parseLoad(all);
  // notes = todo lo que sobra (sin name) si hay extra info.
  const notes = tail || null;
  return {
    name: name || "Sesión",
    intensity,
    duration_min,
    load,
    notes,
    tone,
  };
}

/* Limpia inline markdown básico (negritas, italics) para mostrar texto plano. */
function _stripInline(s) {
  if (!s) return s;
  return String(s)
    .replace(/\*\*([^*]+)\*\*/g, "$1")
    .replace(/__([^_]+)__/g, "$1")
    .replace(/_([^_]+)_/g, "$1")
    .replace(/`([^`]+)`/g, "$1")
    .trim();
}

function parseTrainingPlan(markdown) {
  if (!markdown || typeof markdown !== "string") return null;
  // Algunos modelos envuelven toda la respuesta en ```markdown ... ```. Sin
  // strip, los headings ## quedan dentro de un code block invisible para el
  // resto del parser y la primera "Semana" nunca matchea.
  const stripper = (typeof window !== "undefined" && window.stripWrappingCodeFence)
    ? window.stripWrappingCodeFence
    : (s) => {
        const m = String(s).replace(/\r\n/g, "\n").match(/^\s*```[a-zA-Z]*\s*\n([\s\S]*?)\n```\s*$/);
        return m ? m[1] : s;
      };
  const lines = stripper(markdown).replace(/\r\n/g, "\n").split("\n");

  let title = null;
  const weeks = [];
  let currentWeek = null;
  let summaryLines = [];
  let inSummarySection = false;

  // Pase principal.
  for (let i = 0; i < lines.length; i++) {
    const raw = lines[i];
    const line = raw.trimEnd();
    if (!line.trim()) continue;

    // Título del plan: primer h1 antes de cualquier semana.
    if (!title && /^\s*#\s+/.test(line) && weeks.length === 0) {
      title = _stripInline(line.replace(/^\s*#\s+/, ""));
      continue;
    }

    // ¿Sección "Por qué" / "Resumen" / "Notas"? Activa modo summary.
    const lowered = line.replace(/^\s*#+\s*/, "").trim().toLowerCase();
    if (
      /^\s*#+\s*/.test(line) &&
      /^(por qu(é|e)|resumen|notas|justificaci(ó|o)n|narrativa|why|rationale|summary|observaciones)\b/.test(lowered)
    ) {
      inSummarySection = true;
      currentWeek = null;
      continue;
    }

    // Encabezado de semana.
    const wk = _matchWeekHeading(line);
    if (wk) {
      inSummarySection = false;
      currentWeek = { n: wk.n, label: _stripInline(wk.label), days: [] };
      weeks.push(currentWeek);
      continue;
    }

    // Línea de día (solo si estamos dentro de una semana).
    if (currentWeek) {
      const day = _matchDayLine(line);
      if (day) {
        const session = _parseSession(_stripInline(day.rest));
        // Si el día ya existe en la semana (LLM partió una sesión en 2 líneas), añadimos.
        const existing = currentWeek.days.find((d) => d.dow === day.dow);
        if (existing) {
          if (session) existing.sessions.push(session);
        } else {
          currentWeek.days.push({
            dow: day.dow,
            label: day.label,
            sessions: session ? [session] : [],
          });
        }
        continue;
      }
      // Bullet anidado / continuación. Detectamos "Principal" y "Alternativa"
      // (con o sin negritas) para que el plan multi-deporte se renderice
      // como dos opciones por sesión en vez de quedar como notas planas.
      if (/^\s+[-*•]\s+/.test(raw) || /^\s{2,}\S/.test(raw)) {
        const lastDay = currentWeek.days[currentWeek.days.length - 1];
        if (lastDay && lastDay.sessions.length) {
          const lastSession = lastDay.sessions[lastDay.sessions.length - 1];
          const stripped = _stripInline(line.replace(/^\s*[-*•]\s*/, ""));
          const variantMatch = stripped.match(/^(Principal|Alternativa|Alt|Primary|Alternative)\s*[:：]\s*(.+)$/i);
          if (variantMatch) {
            const kind = /^(alt|alternativ)/i.test(variantMatch[1]) ? "alt" : "primary";
            const body = variantMatch[2].trim();
            const sessionParts = _parseSession(body) || { name: body, notes: null };
            const variant = {
              kind,
              name: sessionParts.name,
              intensity: sessionParts.intensity,
              duration_min: sessionParts.duration_min,
              load: sessionParts.load,
              notes: sessionParts.notes,
              tone: sessionParts.tone || lastSession.tone,
              sport: _inferSport(body),
            };
            lastSession.variants = lastSession.variants || [];
            // Reemplazar variant del mismo kind si el LLM repite (último gana).
            lastSession.variants = lastSession.variants.filter((v) => v.kind !== kind);
            lastSession.variants.push(variant);
            // Si la sesión "principal" del header estaba vacía, propagamos el
            // nombre de la variante principal para que el chip se vea poblado.
            if (kind === "primary" && (!lastSession.intensity && !lastSession.duration_min)) {
              lastSession.intensity = sessionParts.intensity ?? lastSession.intensity;
              lastSession.duration_min = sessionParts.duration_min ?? lastSession.duration_min;
            }
          } else {
            lastSession.notes = lastSession.notes ? `${lastSession.notes} · ${stripped}` : stripped;
          }
        }
        continue;
      }
    }

    // Si no encajó en nada y estamos en summary, acumulamos.
    if (inSummarySection || (!currentWeek && weeks.length > 0)) {
      summaryLines.push(_stripInline(line));
    }
  }

  // Si no encontramos ninguna semana, no podemos parsear → null.
  if (!weeks.length) return null;

  // Asegurar que cada semana tenga 7 entradas (rellenar días vacíos como descanso).
  for (const w of weeks) {
    const present = new Set(w.days.map((d) => d.dow));
    for (let dow = 0; dow < 7; dow++) {
      if (!present.has(dow)) {
        w.days.push({ dow, label: _DOW_LABELS[dow], sessions: [] });
      }
    }
    w.days.sort((a, b) => a.dow - b.dow);
  }

  // Si no hubo summary explícito, agarrar último párrafo largo del documento.
  let summary = summaryLines.join("\n").trim();
  if (!summary) {
    // Buscar el párrafo final más largo después de la última semana.
    const lastWeekIdx = (() => {
      for (let i = lines.length - 1; i >= 0; i--) {
        if (_matchWeekHeading(lines[i].trim())) return i;
      }
      return -1;
    })();
    if (lastWeekIdx >= 0) {
      const tail = lines.slice(lastWeekIdx).join("\n");
      // Tomar el último bloque de texto plano (sin bullets) más largo.
      const blocks = tail.split(/\n\s*\n/).map((b) => b.trim()).filter(Boolean);
      const candidate = blocks
        .reverse()
        .find((b) => b.length > 80 && !_matchWeekHeading(b.split("\n")[0]) && !/^[-*•]/.test(b));
      if (candidate) summary = _stripInline(candidate);
    }
  }

  return {
    title: title || "Plan de entrenamiento",
    summary: summary || "",
    weeks,
  };
}

window.parseTrainingPlan = parseTrainingPlan;
window._planParserInternals = {
  inferTone,
  parseDurationMinutes,
  parseIntensity,
  parseLoad,
};
