// running/src/coach.jsx — slice 7 of the module split.
//
// The AI coach: the full-page Coach view, the slide-in ChatSidebar,
// and their private markdown renderer (renderMarkdown / inlineMd).
// Extracted verbatim from running/index.html. Race-prediction math
// destructures straight from the window.RaceMath UMD lib (same source
// the main block uses); activity classification and the local metric
// calculators come through the render-time window.__appHelpers bridge;
// db/functions resolve from the page-level firebase script.

const { C, Card, Btn, fmt } = window.SharedUI;
const useData = () => (window.__appUseData ? window.__appUseData() : null);
const { isRun, isXT, actDisplay, effectiveType } = window.ActivityTypes;
const calcACRatio = (...a) => window.__appHelpers.calcACRatio(...a);
const calcMarathonShape = (...a) => window.__appHelpers.calcMarathonShape(...a);
const calcWeeklyMetrics = (...a) => window.__appHelpers.calcWeeklyMetrics(...a);
const { calcEffectiveVO2max, predictRaceTimes, calcTrainingPaces } = window.RaceMath;
const { useState, useEffect, useRef } = React;

    // Lightweight markdown renderer for coach messages
    function renderMarkdown(text) {
      if (!text) return null;
      const lines = text.split("\n");
      const elements = [];
      let i = 0;
      while (i < lines.length) {
        const line = lines[i];
        // Table: consecutive lines starting with |
        if (line.trim().startsWith("|")) {
          const tableLines = [];
          while (i < lines.length && lines[i].trim().startsWith("|")) {
            tableLines.push(lines[i]); i++;
          }
          // Filter out separator rows (|---|---|)
          const dataRows = tableLines.filter(l => !l.match(/^\s*\|[\s\-:|]+\|\s*$/));
          if (dataRows.length > 0) {
            const parseRow = r => r.split("|").filter((_, j, a) => j > 0 && j < a.length - 1).map(c => c.trim());
            const header = parseRow(dataRows[0]);
            const body = dataRows.slice(1).map(parseRow);
            elements.push(
              React.createElement("table", { key: `t${i}`, style: { width: "100%", borderCollapse: "collapse", margin: "8px 0", fontSize: 12 } },
                React.createElement("thead", null,
                  React.createElement("tr", null, header.map((h, j) =>
                    React.createElement("th", { key: j, style: { textAlign: "left", padding: "6px 10px", borderBottom: `1px solid ${C.border}`, color: C.textDim, fontSize: 11, fontWeight: 700 } }, inlineMd(h))
                  ))
                ),
                React.createElement("tbody", null, body.map((row, ri) =>
                  React.createElement("tr", { key: ri }, row.map((cell, ci) =>
                    React.createElement("td", { key: ci, style: { padding: "5px 10px", borderBottom: `1px solid ${C.border}30`, color: C.text } }, inlineMd(cell))
                  ))
                ))
              )
            );
          }
          continue;
        }
        // Horizontal rule
        if (line.trim().match(/^-{3,}$|^\*{3,}$/)) {
          elements.push(React.createElement("hr", { key: `hr${i}`, style: { border: "none", borderTop: `1px solid ${C.border}`, margin: "10px 0" } }));
          i++; continue;
        }
        // Headings
        const hMatch = line.match(/^(#{1,3})\s+(.+)/);
        if (hMatch) {
          const level = hMatch[1].length;
          const sz = level === 1 ? 15 : level === 2 ? 13 : 12;
          elements.push(React.createElement("div", { key: `h${i}`, style: { fontSize: sz, fontWeight: 700, color: C.text, margin: "10px 0 4px" } }, inlineMd(hMatch[2])));
          i++; continue;
        }
        // List item
        if (line.match(/^\s*[-*]\s+/)) {
          const items = [];
          while (i < lines.length && lines[i].match(/^\s*[-*]\s+/)) {
            items.push(lines[i].replace(/^\s*[-*]\s+/, "")); i++;
          }
          elements.push(
            React.createElement("ul", { key: `ul${i}`, style: { margin: "4px 0", paddingLeft: 20 } },
              items.map((item, j) => React.createElement("li", { key: j, style: { marginBottom: 3 } }, inlineMd(item)))
            )
          );
          continue;
        }
        // Numbered list
        if (line.match(/^\s*\d+\.\s+/)) {
          const items = [];
          while (i < lines.length && lines[i].match(/^\s*\d+\.\s+/)) {
            items.push(lines[i].replace(/^\s*\d+\.\s+/, "")); i++;
          }
          elements.push(
            React.createElement("ol", { key: `ol${i}`, style: { margin: "4px 0", paddingLeft: 20 } },
              items.map((item, j) => React.createElement("li", { key: j, style: { marginBottom: 3 } }, inlineMd(item)))
            )
          );
          continue;
        }
        // Empty line = spacing
        if (!line.trim()) { elements.push(React.createElement("div", { key: `br${i}`, style: { height: 6 } })); i++; continue; }
        // Regular paragraph
        elements.push(React.createElement("div", { key: `p${i}`, style: { margin: "2px 0" } }, inlineMd(line)));
        i++;
      }
      return React.createElement(React.Fragment, null, ...elements);
    }

    // Inline markdown: **bold**, *italic*, `code`
    function inlineMd(text) {
      if (!text) return text;
      const parts = [];
      let remaining = text;
      let key = 0;
      const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g;
      let lastIndex = 0;
      let match;
      while ((match = regex.exec(text)) !== null) {
        if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
        if (match[2]) parts.push(React.createElement("strong", { key: key++, style: { fontWeight: 700, color: C.text } }, match[2]));
        else if (match[3]) parts.push(React.createElement("em", { key: key++, style: { fontStyle: "italic" } }, match[3]));
        else if (match[4]) parts.push(React.createElement("code", { key: key++, style: { background: C.bg3 || C.bg2, padding: "1px 5px", borderRadius: 4, fontSize: "0.9em" } }, match[4]));
        lastIndex = regex.lastIndex;
      }
      if (lastIndex < text.length) parts.push(text.slice(lastIndex));
      return parts.length ? parts : text;
    }

    // ── Live plan-job progress ──────────────────────────────────────────────
    // The coach's plan tools queue background jobs in users/{uid}/coachActions
    // (processed server-side by the processCoachAction trigger). Subscribing
    // here gives the athlete live week-by-week feedback in the chat — the
    // cascade keeps them informed as each week completes instead of going
    // silent after "queued".
    function usePlanJobs(userId) {
      const [jobs, setJobs] = useState([]);
      useEffect(() => {
        if (!userId) return;
        const unsub = db.collection("users").doc(userId).collection("coachActions")
          .orderBy("createdAt", "desc").limit(6)
          .onSnapshot(snap => {
            const now = Date.now();
            const rows = snap.docs
              .map(d => ({ id: d.id, ...d.data() }))
              .filter(j => j.type === "cascade_refresh" || j.type === "generate_week")
              .filter(j => {
                // Active jobs always show; finished ones linger 30 minutes so
                // the athlete sees the outcome, then drop away.
                if (j.status === "pending" || j.status === "running") return true;
                const doneMs = j.finishedAt?.toDate ? j.finishedAt.toDate().getTime() : null;
                return doneMs != null && (now - doneMs) < 30 * 60 * 1000;
              });
            setJobs(rows.reverse()); // oldest first, reads top-down like the chat
          }, err => console.warn("[planJobs] subscribe failed:", err.message));
        return () => unsub();
      }, [userId]);
      return jobs;
    }

    function PlanJobCard({ job, compact }) {
      const statusMeta = {
        pending:               { icon: "⏳", color: C.amber, label: "Queued — waiting for the server worker" },
        running:               { icon: "🔄", color: C.cyan,  label: "Refreshing week by week…" },
        completed:             { icon: "✅", color: C.green, label: "Done" },
        completed_with_errors: { icon: "⚠️", color: C.amber, label: "Done with errors" },
        continued:             { icon: "⏩", color: C.cyan,  label: "Continuing in a follow-up job…" },
        failed:                { icon: "❌", color: C.red,   label: "Failed" },
      };
      const meta = statusMeta[job.status] || statusMeta.pending;
      const title = job.type === "cascade_refresh"
        ? `Plan refresh — weeks ${job.startWeek}–${job.endWeek}`
        : (job.action === "refresh" ? `Week ${job.weekNum} refresh` : "New week generation");
      const rowIcon = { done: "✓", running: "◌", queued: "·", skipped: "⤼", failed: "✗" };
      const rowColor = { done: C.green, running: C.cyan, queued: C.textMuted, skipped: C.textMuted, failed: C.red };
      return (
        <div style={{
          alignSelf: "stretch", padding: compact ? "8px 12px" : "10px 14px", borderRadius: 10,
          background: `${meta.color}0d`, border: `1px solid ${meta.color}40`,
          fontSize: compact ? 11 : 12, color: C.text, lineHeight: 1.5,
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 6, fontWeight: 700, color: meta.color, fontSize: compact ? 10 : 11 }}>
            <span>{meta.icon}</span><span>{title.toUpperCase()}</span>
            <span style={{ fontWeight: 400, color: C.textMuted }}>— {meta.label}</span>
          </div>
          {Array.isArray(job.progress) && job.progress.length > 0 && (
            <div style={{ marginTop: 6, display: "flex", flexDirection: "column", gap: 2 }}>
              {job.progress.map(p => (
                <div key={p.week} style={{ display: "flex", gap: 8, color: rowColor[p.status] || C.textMuted, fontFamily: "'IBM Plex Mono',monospace", fontSize: compact ? 10 : 11 }}>
                  <span style={{ width: 12, textAlign: "center" }}>{rowIcon[p.status] || "·"}</span>
                  <span style={{ width: 34 }}>W{p.week}</span>
                  <span style={{ flex: 1, color: p.status === "failed" ? C.red : C.textDim }}>
                    {p.status === "running" ? "refreshing…"
                      : p.status === "queued" ? "queued"
                      : p.status === "skipped" ? "skipped (all days locked)"
                      : p.status === "failed" ? (p.error || "failed")
                      : (p.detail || "done")}
                  </span>
                </div>
              ))}
            </div>
          )}
          {job.status === "failed" && job.error && (
            <div style={{ marginTop: 4, color: C.red, fontSize: compact ? 10 : 11 }}>{job.error}</div>
          )}
          {(job.status === "completed" || job.status === "completed_with_errors") && job.summary && (
            <div style={{ marginTop: 4, color: C.textDim, fontSize: compact ? 10 : 11 }}>{job.summary}</div>
          )}
        </div>
      );
    }

    function Coach({ userId, activities, user }) {
      const [messages, setMessages] = useState([]);
      const [input, setInput] = useState("");
      const [loading, setLoading] = useState(false);
      const [weeklyInsight, setWeeklyInsight] = useState(null);
      const [loadingInsight, setLoadingInsight] = useState(false);
      const [pendingImage, setPendingImage] = useState(null); // { data, mediaType, preview }
      const messagesEndRef = useRef(null);
      const fileInputRef = useRef(null);
      const planJobs = usePlanJobs(userId);

      // Pre-fill the input when arriving via askCoach() from another page.
      const { consumeCoachPrompt, athleteProfile } = useData() || {};
      useEffect(() => {
        const p = consumeCoachPrompt && consumeCoachPrompt();
        if (p) setInput(p);
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);

      const handleImageUpload = (e) => {
        const file = e.target.files?.[0];
        if (!file) return;
        if (!file.type.startsWith("image/")) return;
        const reader = new FileReader();
        reader.onload = () => {
          const base64 = reader.result.split(",")[1];
          setPendingImage({ data: base64, mediaType: file.type, preview: reader.result });
        };
        reader.readAsDataURL(file);
        e.target.value = "";
      };

      useEffect(() => {
        if (!userId) return;
        db.collection("users").doc(userId).collection("coaching")
          .orderBy("timestamp","desc").limit(1)
          .get().then(snap => {
            if (!snap.empty) {
              const data = snap.docs[0].data();
              if (data.messages) setMessages(data.messages);
            } else {
              setMessages([{
                role:"assistant",
                content:`Hey Chris! 👋 I'm your AI running coach. I have access to your Strava data — ${activities.filter(a=>a.type==="Run"||a.type==="VirtualRun").length} runs synced. Ask me anything about your training, race preparation, recovery, or nutrition. What's on your mind?`
              }]);
            }
          });
      }, [userId]);

      useEffect(() => {
        messagesEndRef.current?.scrollIntoView({ behavior:"smooth" });
      }, [messages, planJobs]);

      const sendMessage = async () => {
        if ((!input.trim() && !pendingImage) || loading) return;
        const userMsg = { role:"user", content: input || (pendingImage ? "Here's an image — please analyze it." : "") };
        if (pendingImage) {
          userMsg.image = { data: pendingImage.data, mediaType: pendingImage.mediaType };
          userMsg.imagePreview = pendingImage.preview; // for display only
        }
        const newMessages = [...messages, userMsg];
        setMessages(newMessages);
        setInput("");
        setPendingImage(null);
        setLoading(true);

        try {
          // Robust ms-since-epoch helper. Without this, mixed-format
          // start_date values (Firestore Timestamp vs ISO string vs raw
          // number) sort unpredictably and today's run can drop out of
          // slice(0,10), producing the "I don't see this morning's run"
          // failure mode.
          const tsMs = (a) => {
            const s = a?.start_date;
            if (!s) return 0;
            if (typeof s.toDate === "function") return s.toDate().getTime();
            if (typeof s.seconds === "number") return s.seconds * 1000;
            if (typeof s === "string") return Date.parse(s) || 0;
            if (typeof s === "number") return s < 1e12 ? s * 1000 : s;
            return 0;
          };
          // Human-readable date label so Claude doesn't have to convert
          // a unix timestamp to "today" / "yesterday" mentally — without
          // this it routinely says "I don't see this morning's run"
          // even when the run is in the context.
          const _todayStart = (() => { const d = new Date(); d.setHours(0,0,0,0); return d.getTime(); })();
          const labelDate = (ms) => {
            if (!ms) return null;
            const diffDays = Math.floor((_todayStart - ms) / 86400000);
            const iso = new Date(ms).toISOString().slice(0, 10);
            if (diffDays === 0) return `${iso} (today)`;
            if (diffDays === 1) return `${iso} (yesterday)`;
            if (diffDays > 0 && diffDays < 7) return `${iso} (${diffDays}d ago)`;
            return iso;
          };
          const runs = activities.filter(a=>isRun(a));
          const recentRuns = runs.slice().sort((a,b)=>tsMs(b)-tsMs(a)).slice(0,10);
          const weeklyMiles = recentRuns.slice(0,7).reduce((s,r)=>s+(r.distance/1609.34),0);

          // Include cross-training context
          const recentXT = activities.filter(a=>isXT(a))
            .sort((a,b)=>b.start_date?.seconds-a.start_date?.seconds).slice(0,5);

          // Compute performance metrics for coach context
          const coachVO2 = calcEffectiveVO2max(runs);
          const coachShape = calcMarathonShape(runs);
          const coachRacePreds = predictRaceTimes(coachVO2);
          const coachPaces = calcTrainingPaces(coachVO2, runs);
          const coachAC = calcACRatio(activities, athleteProfile);
          const coachWeekly = calcWeeklyMetrics(activities, 0, athleteProfile);

          // Strip imagePreview from messages sent to backend (too large for display-only field)
          const apiMessages = newMessages.map(m => {
            const msg = { role: m.role, content: m.content };
            if (m.image) msg.image = m.image;
            return msg;
          });

          // Garmin dynamics from recent enriched runs (newest first → reverse for chronological)
          const recentDynamics = runs
            .filter(r => r.dynamics && (r.dynamics.groundContactTime || r.dynamics.cadenceSpm || r.dynamics.verticalOscillation))
            .slice(0, 12)
            .reverse()
            .map(r => {
              const d = r.dynamics;
              return {
                date: labelDate(tsMs(r)),
                name: r.name,
                distanceMi: r.distance > 0 ? parseFloat((r.distance/1609.34).toFixed(1)) : null,
                avgHR: r.average_heartrate || null,
                maxHR: r.max_heartrate || null,
                isIndoor: d.isIndoor || false,
                // Form dynamics
                gct: d.groundContactTime || null,
                gctBalance: d.groundContactBalance || null,
                vertOsc: d.verticalOscillation || null,
                vertRatio: d.verticalRatio || null,
                strideLength: d.strideLengthSensor || null,
                cadenceSpm: d.cadenceSpm || null,
                maxCadenceSpm: d.maxCadenceSpm || null,
                // Step Speed Loss — Garmin running-economy metric (cm/s
                // bled at each ground contact). Lower = more efficient.
                stepSpeedLoss: d.stepSpeedLoss ?? null,
                stepSpeedLossPct: d.stepSpeedLossPct ?? null,
                // Power
                power: d.power || null,
                maxPower: d.maxPower || null,
                normalizedPower: d.normalizedPower || null,
                // Training effect & load
                trainingEffect: d.trainingEffect || null,
                anaerobicTE: d.anaerobicTrainingEffect || null,
                trainingEffectLabel: d.trainingEffectLabel || null,
                exerciseLoad: d.exerciseLoad || null,
                tss: d.trainingStressScore || null,
                recoveryTime: d.recoveryTime || null,
                performanceCondition: d.performanceCondition ?? null,
                // Stamina
                beginningStamina: d.beginningStamina ?? null,
                endingStamina: d.endingStamina ?? null,
                minStamina: d.minStamina ?? null,
                // Physiology
                respirationRate: d.respirationRate || null,
                minRespirationRate: d.minRespirationRate || null,
                garminVO2max: d.garminVO2max || null,
                lthr: d.lactateThresholdHR || null,
                // Environment & energy
                avgTemperature: d.avgTemperature ?? null,
                activeCalories: d.activeCalories || null,
                impactLoad: d.impactLoad || null,
                // Run/walk split
                runTimeMins: d.runTimeSeconds ? parseFloat((d.runTimeSeconds/60).toFixed(1)) : null,
                walkTimeMins: d.walkTimeSeconds ? parseFloat((d.walkTimeSeconds/60).toFixed(1)) : null,
              };
            });

          // 180s to match the server budget — the agentic tool loop can make
          // several model calls per turn; the default 70s cuts it off early.
          const generateCoachingInsight = functions.httpsCallable("generateCoachingInsight", { timeout: 180000 });
          const result = await generateCoachingInsight({
            messages: apiMessages,
            context: {
              currentDate: new Date().toISOString().slice(0, 10),
              totalRuns: runs.length,
              recentRuns: recentRuns.map(r => ({
                name: r.name,
                date: labelDate(tsMs(r)),
                distanceMiles: (r.distance/1609.34).toFixed(2),
                duration: fmt.duration(r.moving_time),
                pace: fmt.pace(r.moving_time/(r.distance/1609.34)),
                avgHR: r.average_heartrate,
              })),
              weeklyMiles: weeklyMiles.toFixed(1),
              crossTraining: recentXT.map(a => ({
                name: a.name,
                type: effectiveType(a),
                sport: actDisplay(effectiveType(a)).label,
                date: a.start_date?.seconds,
                duration: fmt.duration(a.moving_time),
                distanceMiles: a.distance > 100 ? (a.distance/1609.34).toFixed(2) : null,
                avgHR: a.average_heartrate,
                calories: a.calories,
              })),
              totalXTActivities: activities.filter(a=>isXT(a)).length,
              effectiveVO2max: coachVO2,
              marathonShape: coachShape,
              racePredictions: coachRacePreds ? coachRacePreds.map(rp => ({ name: rp.name, seconds: rp.seconds })) : [],
              trainingPaces: coachPaces ? coachPaces.map(tp => ({ zone: tp.zone, abbr: tp.abbr, min: tp.min ? fmt.pace(tp.min) : null, max: tp.max ? fmt.pace(tp.max) : null })) : [],
              workloadRatio: coachAC.ratio,
              weeklyTRIMP: coachWeekly.totalTRIMP,
              monotony: coachWeekly.monotony,
              strain: coachWeekly.strain,
              formDynamics: recentDynamics,
            }
          });

          // Build assistant message with tool result badges
          let responseText = result.data.response;
          const toolResults = result.data.toolResults || [];
          const toolBadges = toolResults.filter(t => t.success).map(t => {
            const labels = {
              save_supplements: "💊 Supplements saved",
              save_nutrition_targets: "🍽️ Nutrition targets saved",
              save_race_goal: "🏁 Race goal saved",
              save_injury_note: "🩹 Injury logged",
              update_training_day: "📅 Training day updated",
              generate_training_week: "📋 Week job queued",
              refresh_plan_weeks: "🔁 Plan cascade started",
              push_workout_to_garmin: "⌚ Workout pushed to Garmin",
              save_hr_zones: "❤️ HR zones updated",
            };
            return labels[t.tool] || t.tool;
          });

          const assistantMsg = { role:"assistant", content: responseText, toolBadges };
          const finalMessages = [...newMessages, assistantMsg];
          setMessages(finalMessages);

          // Save conversation (strip image data to keep Firestore docs small)
          const saveMessages = finalMessages.map(m => {
            const s = { role: m.role, content: m.content };
            if (m.toolBadges) s.toolBadges = m.toolBadges;
            if (m.image) s.hadImage = true;
            return s;
          });
          await db.collection("users").doc(userId).collection("coaching").add({
            messages: saveMessages,
            timestamp: firebase.firestore.FieldValue.serverTimestamp(),
          });
        } catch (err) {
          setMessages([...newMessages, { role:"assistant", content:"Sorry, I had trouble connecting. " + (err.message || "") }]);
        }
        setLoading(false);
      };

      const getWeeklyInsight = async () => {
        setLoadingInsight(true);
        try {
          // Same robust ms-since-epoch + relative-date label helpers
          // used in the chat coach above. Both surfaces need the same
          // logic so today's run shows up consistently.
          const tsMs = (a) => {
            const s = a?.start_date;
            if (!s) return 0;
            if (typeof s.toDate === "function") return s.toDate().getTime();
            if (typeof s.seconds === "number") return s.seconds * 1000;
            if (typeof s === "string") return Date.parse(s) || 0;
            if (typeof s === "number") return s < 1e12 ? s * 1000 : s;
            return 0;
          };
          const _todayStart = (() => { const d = new Date(); d.setHours(0,0,0,0); return d.getTime(); })();
          const labelDate = (ms) => {
            if (!ms) return null;
            const diffDays = Math.floor((_todayStart - ms) / 86400000);
            const iso = new Date(ms).toISOString().slice(0, 10);
            if (diffDays === 0) return `${iso} (today)`;
            if (diffDays === 1) return `${iso} (yesterday)`;
            if (diffDays > 0 && diffDays < 7) return `${iso} (${diffDays}d ago)`;
            return iso;
          };
          const recentInsightRuns = activities.filter(a=>isRun(a))
            .slice().sort((a,b)=>tsMs(b)-tsMs(a)).slice(0,7);
          const recentInsightXT = activities.filter(a=>isXT(a))
            .sort((a,b)=>b.start_date?.seconds-a.start_date?.seconds).slice(0,5);
          const insightRuns = activities.filter(a=>isRun(a));
          const insightVO2 = calcEffectiveVO2max(insightRuns);
          const insightShape = calcMarathonShape(insightRuns);
          const insightAC = calcACRatio(activities, athleteProfile);
          const insightWeekly = calcWeeklyMetrics(activities, 0, athleteProfile);

          const insightDynamics = activities
            .filter(a => isRun(a) && a.dynamics && (a.dynamics.groundContactTime || a.dynamics.cadenceSpm))
            .sort((a,b) => b.start_date?.seconds - a.start_date?.seconds)
            .slice(0, 12).reverse()
            .map(r => {
              const d = r.dynamics;
              return {
                date: labelDate(tsMs(r)), name: r.name,
                distanceMi: r.distance > 0 ? parseFloat((r.distance/1609.34).toFixed(1)) : null,
                avgHR: r.average_heartrate || null,
                isIndoor: d.isIndoor || false,
                gct: d.groundContactTime || null,
                gctBalance: d.groundContactBalance || null,
                vertOsc: d.verticalOscillation || null,
                vertRatio: d.verticalRatio || null,
                strideLength: d.strideLengthSensor || null,
                cadenceSpm: d.cadenceSpm || null,
                stepSpeedLoss: d.stepSpeedLoss ?? null,
                stepSpeedLossPct: d.stepSpeedLossPct ?? null,
                power: d.power || null,
                normalizedPower: d.normalizedPower || null,
                trainingEffect: d.trainingEffect || null,
                anaerobicTE: d.anaerobicTrainingEffect || null,
                trainingEffectLabel: d.trainingEffectLabel || null,
                exerciseLoad: d.exerciseLoad || null,
                tss: d.trainingStressScore || null,
                recoveryTime: d.recoveryTime || null,
                performanceCondition: d.performanceCondition ?? null,
                beginningStamina: d.beginningStamina ?? null,
                endingStamina: d.endingStamina ?? null,
                minStamina: d.minStamina ?? null,
                respirationRate: d.respirationRate || null,
                garminVO2max: d.garminVO2max || null,
                lthr: d.lactateThresholdHR || null,
                avgTemperature: d.avgTemperature ?? null,
                activeCalories: d.activeCalories || null,
                impactLoad: d.impactLoad || null,
                runTimeMins: d.runTimeSeconds ? parseFloat((d.runTimeSeconds/60).toFixed(1)) : null,
                walkTimeMins: d.walkTimeSeconds ? parseFloat((d.walkTimeSeconds/60).toFixed(1)) : null,
              };
            });

          const generateCoachingInsight = functions.httpsCallable("generateCoachingInsight", { timeout: 180000 });
          const result = await generateCoachingInsight({
            messages: [{ role:"user", content:"Give me a weekly training summary and key insights in 3-4 bullet points. Include cross-training analysis and any form/dynamics observations if data is available." }],
            context: {
              totalRuns: activities.filter(a=>isRun(a)).length,
              recentRuns: recentInsightRuns.map(r => ({
                name:r.name, distanceMiles:(r.distance/1609.34).toFixed(2),
                duration:fmt.duration(r.moving_time), pace:fmt.pace(r.moving_time/(r.distance/1609.34)),
                avgHR:r.average_heartrate,
              })),
              weeklyMiles: recentInsightRuns.reduce((s,r)=>s+(r.distance/1609.34),0).toFixed(1),
              crossTraining: recentInsightXT.map(a => ({
                name: a.name, type: effectiveType(a), sport: actDisplay(effectiveType(a)).label,
                duration: fmt.duration(a.moving_time), avgHR: a.average_heartrate,
              })),
              totalXTActivities: activities.filter(a=>isXT(a)).length,
              effectiveVO2max: insightVO2,
              marathonShape: insightShape,
              racePredictions: predictRaceTimes(insightVO2)?.map(rp => ({ name: rp.name, seconds: rp.seconds })) || [],
              workloadRatio: insightAC.ratio,
              weeklyTRIMP: insightWeekly.totalTRIMP,
              monotony: insightWeekly.monotony,
              strain: insightWeekly.strain,
              formDynamics: insightDynamics,
            }
          });
          setWeeklyInsight(result.data.response);
        } catch (err) {
          setWeeklyInsight("Unable to generate insight right now.");
        }
        setLoadingInsight(false);
      };

      // Quick prompts — form analysis as a first-class action
      const quickPrompts = [
        { label: "Form Analysis", icon: "📐", prompt: "Analyze my running form from the Garmin dynamics data. What is my #1 weakness right now, and give me a specific targeted session or drill to fix it this week without disrupting my overall training plan." },
        { label: "Weekly Brief", icon: "📋", prompt: "Give me this week's coaching brief: training load assessment, top 1-2 form priorities based on my dynamics, and specific session prescriptions for the week ahead." },
        { label: "Session Design", icon: "🎯", prompt: "Based on my form weaknesses, design a complete targeted session that addresses them. Include warm-up, main set, and cool-down with specific cues and targets." },
        { label: "Injury Risk", icon: "🩹", prompt: "Review my recent dynamics and training load for injury risk patterns. Any red flags in my L/R balance, ground contact, or overtraining indicators?" },
      ];

      const runsWithForm = activities.filter(a => isRun(a) && a.dynamics && (a.dynamics.groundContactTime || a.dynamics.cadenceSpm));

      return (
        <div style={{display:"flex",flexDirection:"column",gap:20,height:"calc(100vh - 160px)"}}>
          <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
            <div>
              <div style={{fontFamily:"'Space Grotesk', sans-serif",fontSize:26,fontWeight:800,color:C.text,marginBottom:4}}>Coach</div>
              <div style={{color:C.textMuted,fontSize:13}}>Powered by Claude · {runsWithForm.length > 0 ? `${runsWithForm.length} runs with form data` : "no dynamics yet"}</div>
            </div>
            <Btn onClick={getWeeklyInsight} disabled={loadingInsight} variant="secondary" small>
              {loadingInsight ? "..." : "Weekly Insight"}
            </Btn>
          </div>

          {/* Quick-prompt bar */}
          <div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
            {quickPrompts.map(qp => (
              <button key={qp.label} onClick={() => { setInput(qp.prompt); }} style={{
                padding:"6px 12px", borderRadius:20, border:`1px solid ${C.border}`,
                background:"transparent", color:C.textDim, fontSize:12, cursor:"pointer",
                display:"flex",alignItems:"center",gap:5, fontFamily:"inherit",
                transition:"border-color 0.15s",
              }}
              onMouseEnter={e=>e.currentTarget.style.borderColor=C.cyan}
              onMouseLeave={e=>e.currentTarget.style.borderColor=C.border}>
                <span>{qp.icon}</span>{qp.label}
              </button>
            ))}
          </div>

          {weeklyInsight && (
            <Card style={{borderColor:C.purple+"60",background:`${C.purple}08`}}>
              <div style={{fontSize:12,fontWeight:700,color:C.purple,marginBottom:8}}>WEEKLY INSIGHT</div>
              <div style={{fontSize:13,color:C.textDim,lineHeight:1.7}}>{renderMarkdown(weeklyInsight)}</div>
            </Card>
          )}

          {/* Chat */}
          <Card style={{flex:1,display:"flex",flexDirection:"column",overflow:"hidden",padding:0}}>
            {/* Messages */}
            <div style={{flex:1,overflowY:"auto",padding:20,display:"flex",flexDirection:"column",gap:12}}>
              {messages.map((msg, i) => (
                <div key={i} style={{
                  display:"flex",justifyContent: msg.role==="user" ? "flex-end" : "flex-start",
                }}>
                  <div style={{
                    maxWidth:"80%",padding:"12px 16px",borderRadius:12,
                    background: msg.role==="user" ? C.cyan+"20" : C.bg2,
                    border: `1px solid ${msg.role==="user" ? C.cyan+"40" : C.border}`,
                    fontSize:13,color:C.text,lineHeight:1.6,
                    borderTopRightRadius: msg.role==="user" ? 4 : 12,
                    borderTopLeftRadius: msg.role==="assistant" ? 4 : 12,
                  }}>
                    {msg.role==="assistant" && (
                      <div style={{fontSize:10,color:C.cyan,marginBottom:6,fontWeight:700}}>COACH</div>
                    )}
                    {msg.imagePreview && (
                      <img src={msg.imagePreview} alt="Upload" style={{maxWidth:"100%",maxHeight:200,borderRadius:8,marginBottom:8,display:"block"}} />
                    )}
                    {msg.hadImage && !msg.imagePreview && (
                      <div style={{fontSize:11,color:C.textMuted,marginBottom:6,fontStyle:"italic"}}>[ Image attached ]</div>
                    )}
                    {msg.role === "assistant" ? renderMarkdown(msg.content) : msg.content}
                    {msg.toolBadges && msg.toolBadges.length > 0 && (
                      <div style={{display:"flex",flexWrap:"wrap",gap:6,marginTop:8}}>
                        {msg.toolBadges.map((badge, bi) => (
                          <span key={bi} style={{fontSize:10,padding:"3px 8px",borderRadius:20,background:C.green+"20",color:C.green,border:`1px solid ${C.green}40`,fontWeight:600}}>
                            {badge}
                          </span>
                        ))}
                      </div>
                    )}
                  </div>
                </div>
              ))}
              {planJobs.map(job => <PlanJobCard key={job.id} job={job} />)}
              {loading && (
                <div style={{display:"flex",gap:6,padding:"12px 16px",background:C.bg2,borderRadius:12,width:"fit-content",border:`1px solid ${C.border}`}}>
                  {[0,1,2].map(i => (
                    <div key={i} style={{width:6,height:6,background:C.cyan,borderRadius:"50%",
                      animation:`pulse 1s ease-in-out ${i*0.2}s infinite alternate`}} />
                  ))}
                </div>
              )}
              <div ref={messagesEndRef} />
            </div>

            {/* Pending image preview */}
            {pendingImage && (
              <div style={{padding:"8px 20px",borderTop:`1px solid ${C.border}`,display:"flex",alignItems:"center",gap:10}}>
                <img src={pendingImage.preview} alt="Pending" style={{height:48,borderRadius:6,border:`1px solid ${C.border}`}} />
                <span style={{fontSize:12,color:C.textMuted,flex:1}}>Image attached</span>
                <button onClick={() => setPendingImage(null)} style={{background:"none",border:"none",color:C.red,cursor:"pointer",fontSize:16,padding:4}}>x</button>
              </div>
            )}
            {/* Input */}
            <div style={{padding:"16px 20px",borderTop:`1px solid ${C.border}`,display:"flex",gap:10,alignItems:"center"}}>
              <input type="file" accept="image/*" ref={fileInputRef} onChange={handleImageUpload} style={{display:"none"}} />
              <button onClick={() => fileInputRef.current?.click()} title="Upload image" style={{
                background:"none",border:`1px solid ${C.border}`,borderRadius:8,padding:"8px 10px",
                cursor:"pointer",color:C.textMuted,fontSize:16,display:"flex",alignItems:"center",
              }}>
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
                </svg>
              </button>
              <input
                value={input}
                onChange={e => setInput(e.target.value)}
                onKeyDown={e => e.key==="Enter" && !e.shiftKey && sendMessage()}
                placeholder="Ask your coach anything... upload supplements, race plans, screenshots"
                style={{
                  flex:1,background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,
                  padding:"10px 14px",color:C.text,fontSize:13,fontFamily:"'IBM Plex Mono',monospace",
                  outline:"none",
                }}
              />
              <Btn onClick={sendMessage} disabled={loading || (!input.trim() && !pendingImage)}>Send</Btn>
            </div>
          </Card>

          <style>{`
            @keyframes pulse { from { opacity:0.3; transform:scale(0.8); } to { opacity:1; transform:scale(1.1); } }
          `}</style>
        </div>
      );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CHAT SIDEBAR (persistent across all pages)
    // ─────────────────────────────────────────────────────────────────────────
    function ChatSidebar({ userId, activities, open, onToggle }) {
      const { athleteProfile } = useData() || {};
      const [messages, setMessages] = useState([]);
      const [input, setInput] = useState("");
      const [loading, setLoading] = useState(false);
      const messagesEndRef = useRef(null);
      const planJobs = usePlanJobs(userId);

      // Load last conversation
      useEffect(() => {
        if (!userId) return;
        db.collection("users").doc(userId).collection("coaching")
          .orderBy("timestamp","desc").limit(1)
          .get().then(snap => {
            if (!snap.empty) {
              const data = snap.docs[0].data();
              if (data.messages) setMessages(data.messages);
            } else {
              setMessages([{
                role:"assistant",
                content:`Hey! 👋 I'm your AI coach. Ask me anything about your training, paces, race prep, or recovery.`
              }]);
            }
          });
      }, [userId]);

      useEffect(() => {
        if (open) messagesEndRef.current?.scrollIntoView({ behavior:"smooth" });
      }, [messages, open, planJobs]);

      const sendMessage = async () => {
        if (!input.trim() || loading) return;
        const userMsg = { role:"user", content: input };
        const newMessages = [...messages, userMsg];
        setMessages(newMessages);
        setInput("");
        setLoading(true);

        try {
          // Robust ms-since-epoch helper. Without this, mixed-format
          // start_date values (Firestore Timestamp vs ISO string vs raw
          // number) sort unpredictably and today's run can drop out of
          // slice(0,10), producing the "I don't see this morning's run"
          // failure mode.
          const tsMs = (a) => {
            const s = a?.start_date;
            if (!s) return 0;
            if (typeof s.toDate === "function") return s.toDate().getTime();
            if (typeof s.seconds === "number") return s.seconds * 1000;
            if (typeof s === "string") return Date.parse(s) || 0;
            if (typeof s === "number") return s < 1e12 ? s * 1000 : s;
            return 0;
          };
          // Human-readable date label so Claude doesn't have to convert
          // a unix timestamp to "today" / "yesterday" mentally — without
          // this it routinely says "I don't see this morning's run"
          // even when the run is in the context.
          const _todayStart = (() => { const d = new Date(); d.setHours(0,0,0,0); return d.getTime(); })();
          const labelDate = (ms) => {
            if (!ms) return null;
            const diffDays = Math.floor((_todayStart - ms) / 86400000);
            const iso = new Date(ms).toISOString().slice(0, 10);
            if (diffDays === 0) return `${iso} (today)`;
            if (diffDays === 1) return `${iso} (yesterday)`;
            if (diffDays > 0 && diffDays < 7) return `${iso} (${diffDays}d ago)`;
            return iso;
          };
          const runs = activities.filter(a=>isRun(a));
          const recentRuns = runs.slice().sort((a,b)=>tsMs(b)-tsMs(a)).slice(0,10);
          const weeklyMiles = recentRuns.slice(0,7).reduce((s,r)=>s+(r.distance/1609.34),0);
          const recentXT = activities.filter(a=>isXT(a))
            .sort((a,b)=>b.start_date?.seconds-a.start_date?.seconds).slice(0,5);

          const coachVO2 = calcEffectiveVO2max(runs);
          const coachShape = calcMarathonShape(runs);
          const coachRacePreds = predictRaceTimes(coachVO2);
          const coachPaces = calcTrainingPaces(coachVO2, runs);
          const coachAC = calcACRatio(activities, athleteProfile);
          const coachWeekly = calcWeeklyMetrics(activities, 0, athleteProfile);

          const fn = functions.httpsCallable("generateCoachingInsight", { timeout: 180000 });
          const result = await fn({
            messages: newMessages,
            context: {
              currentDate: new Date().toISOString().slice(0, 10),
              totalRuns: runs.length,
              recentRuns: recentRuns.map(r => ({
                name: r.name,
                date: labelDate(tsMs(r)),
                distanceMiles: (r.distance/1609.34).toFixed(2),
                duration: fmt.duration(r.moving_time),
                pace: fmt.pace(r.moving_time/(r.distance/1609.34)),
                avgHR: r.average_heartrate,
              })),
              weeklyMiles: weeklyMiles.toFixed(1),
              crossTraining: recentXT.map(a => ({
                name: a.name, type: effectiveType(a), sport: actDisplay(effectiveType(a)).label,
                date: a.start_date?.seconds,
                duration: fmt.duration(a.moving_time),
                distanceMiles: a.distance > 100 ? (a.distance/1609.34).toFixed(2) : null,
                avgHR: a.average_heartrate,
              })),
              totalXTActivities: activities.filter(a=>isXT(a)).length,
              effectiveVO2max: coachVO2,
              marathonShape: coachShape,
              racePredictions: coachRacePreds ? coachRacePreds.map(rp => ({ name: rp.name, seconds: rp.seconds })) : [],
              trainingPaces: coachPaces ? coachPaces.map(tp => ({ zone: tp.zone, abbr: tp.abbr, min: tp.min ? fmt.pace(tp.min) : null, max: tp.max ? fmt.pace(tp.max) : null })) : [],
              workloadRatio: coachAC.ratio,
              weeklyTRIMP: coachWeekly.totalTRIMP,
              monotony: coachWeekly.monotony,
              strain: coachWeekly.strain,
            }
          });

          const assistantMsg = { role:"assistant", content: result.data.response };
          const finalMessages = [...newMessages, assistantMsg];
          setMessages(finalMessages);

          await db.collection("users").doc(userId).collection("coaching").add({
            messages: finalMessages,
            timestamp: firebase.firestore.FieldValue.serverTimestamp(),
          });
        } catch (err) {
          setMessages([...newMessages, { role:"assistant", content:"Sorry, I had trouble connecting. Please try again." }]);
        }
        setLoading(false);
      };

      return (
        <>
          {/* Toggle button — always visible */}
          <button onClick={onToggle} style={{
            position:"fixed", right: open ? 364 : 0, top: 64, zIndex: 200,
            width: 36, height: 36, borderRadius: "8px 0 0 8px", border: "none",
            background: C.cyan + "20", backdropFilter: "blur(8px)",
            color: C.cyan, fontSize: 16, cursor: "pointer",
            display: "flex", alignItems: "center", justifyContent: "center",
            transition: "right 0.25s ease",
            borderRight: `1px solid ${C.border}`,
          }} title={open ? "Close chat" : "Open coach chat"}>
            {open ? "›" : "‹"}
          </button>

          {/* Sidebar panel */}
          <div style={{
            position:"fixed", right: open ? 0 : -360, top: 56, bottom: 0, width: 360,
            background: C.bg1, borderLeft: `1px solid ${C.border}`,
            display: "flex", flexDirection: "column", zIndex: 150,
            transition: "right 0.25s ease",
            boxShadow: open ? "-4px 0 20px rgba(0,0,0,0.3)" : "none",
          }}>
            {/* Header */}
            <div style={{
              padding: "14px 16px", borderBottom: `1px solid ${C.border}`,
              display: "flex", alignItems: "center", gap: 10,
            }}>
              <div style={{ width: 28, height: 28, borderRadius: 14, background: `${C.cyan}20`,
                display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14 }}>
                🤖
              </div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>AI Coach</div>
                <div style={{ fontSize: 10, color: C.textMuted }}>Powered by Claude</div>
              </div>
              <button onClick={() => {
                setMessages([{ role:"assistant", content:"Fresh conversation started. What would you like to work on?" }]);
              }} style={{
                background: "transparent", border: `1px solid ${C.border}`, borderRadius: 4,
                color: C.textMuted, fontSize: 10, padding: "3px 8px", cursor: "pointer",
                fontFamily: "'IBM Plex Mono',monospace",
              }}>New Chat</button>
            </div>

            {/* Messages */}
            <div style={{ flex: 1, overflowY: "auto", padding: "12px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
              {messages.map((msg, i) => (
                <div key={i} style={{
                  display: "flex", justifyContent: msg.role === "user" ? "flex-end" : "flex-start",
                }}>
                  <div style={{
                    maxWidth: "88%", padding: "8px 12px", borderRadius: 10,
                    background: msg.role === "user" ? C.cyan + "20" : C.bg2,
                    border: `1px solid ${msg.role === "user" ? C.cyan + "30" : C.border}`,
                    fontSize: 12, color: C.text, lineHeight: 1.6,
                    borderTopRightRadius: msg.role === "user" ? 2 : 10,
                    borderTopLeftRadius: msg.role === "assistant" ? 2 : 10,
                  }}>
                    {msg.role === "assistant" && (
                      <div style={{ fontSize: 9, color: C.cyan, marginBottom: 4, fontWeight: 700 }}>COACH</div>
                    )}
                    {msg.role === "assistant" ? renderMarkdown(msg.content) : msg.content}
                  </div>
                </div>
              ))}
              {planJobs.map(job => <PlanJobCard key={job.id} job={job} compact />)}
              {loading && (
                <div style={{ display: "flex", gap: 4, padding: "8px 12px", background: C.bg2, borderRadius: 10, width: "fit-content", border: `1px solid ${C.border}` }}>
                  {[0,1,2].map(i => (
                    <div key={i} style={{ width: 5, height: 5, background: C.cyan, borderRadius: "50%",
                      animation: `pulse 1s ease-in-out ${i*0.2}s infinite alternate` }} />
                  ))}
                </div>
              )}
              <div ref={messagesEndRef} />
            </div>

            {/* Input */}
            <div style={{ padding: "10px 12px", borderTop: `1px solid ${C.border}`, display: "flex", gap: 8 }}>
              <input
                value={input}
                onChange={e => setInput(e.target.value)}
                onKeyDown={e => e.key === "Enter" && !e.shiftKey && sendMessage()}
                placeholder="Ask your coach..."
                style={{
                  flex: 1, background: C.bg2, border: `1px solid ${C.border}`, borderRadius: 6,
                  padding: "8px 10px", color: C.text, fontSize: 12, fontFamily: "'IBM Plex Mono',monospace",
                  outline: "none",
                }}
              />
              <button onClick={sendMessage} disabled={loading || !input.trim()} style={{
                padding: "8px 12px", borderRadius: 6, border: "none",
                background: input.trim() ? C.cyan : C.bg3,
                color: input.trim() ? "#000" : C.textMuted,
                fontSize: 11, fontWeight: 700, cursor: input.trim() ? "pointer" : "not-allowed",
                fontFamily: "'IBM Plex Mono',monospace",
              }}>Send</button>
            </div>
          </div>

          <style>{`
            @keyframes pulse { from { opacity:0.3; transform:scale(0.8); } to { opacity:1; transform:scale(1.1); } }
          `}</style>
        </>
      );
    }

window.AppViews = Object.assign(window.AppViews || {}, { Coach, ChatSidebar });
