// ================= State persistence adapter =================
    function save() {
      saveState();
    }

    // ================= Business rules/helpers =================
    function visibleWins() {
      return state.wins.filter(w => !w.isHidden && (state.filter === "All" || w.category === state.filter));
    }

    function weekKey(dateValue = new Date()) {
      const date = new Date(dateValue);
      const first = new Date(date.getFullYear(), 0, 1);
      const dayMs = 86400000;
      return `${date.getFullYear()}-W${Math.ceil((((date - first) / dayMs) + first.getDay() + 1) / 7)}`;
    }

    function eventWeek(event) {
      return weekKey(event.createdAt || new Date());
    }

    function weeklyPoints(type) {
      const currentWeek = weekKey();
      return state.pointEvents
        .filter(event => event.type === type && event.points > 0 && eventWeek(event) === currentWeek)
        .reduce((sum, event) => sum + event.points, 0);
    }

    function hasEvent(type) {
      const currentWeek = weekKey();
      return state.pointEvents.some(event => event.type === type && eventWeek(event) === currentWeek);
    }

    function awardPoints(type, points, description, options = {}) {
      if (options.onceWeekly && hasEvent(type)) return false;
      if (options.weeklyCap && weeklyPoints(type) >= options.weeklyCap) return false;
      const awarded = options.weeklyCap ? Math.min(points, options.weeklyCap - weeklyPoints(type)) : points;
      if (awarded <= 0) return false;
      state.pointsBalance += awarded;
      state.pointEvents.unshift({
        id: crypto.randomUUID(),
        patientId: patient.id,
        type,
        points: awarded,
        createdAt: new Date().toISOString().slice(0, 10),
        description
      });
      save();
      return true;
    }

    function awardRule(type, description) {
      const rule = POINT_RULES[type];
      if (!rule) return false;
      return awardPoints(type, rule.points, description, rule);
    }

    function normalizeRewardStatus(status) {
      const value = String(status || "").trim().toLowerCase();
      return Object.values(REWARD_STATUSES).includes(value) ? value : REWARD_STATUSES.LOCKED;
    }

    function rewardStatusLabel(status) {
      return REWARD_STATUS_LABELS[normalizeRewardStatus(status)] || String(status || "");
    }

    function requestReward(reward) {
      const existing = state.redemptions.find(item => item.rewardId === reward.id && normalizeRewardStatus(item.status) !== REWARD_STATUSES.REDEEMED);
      if (existing || state.pointsBalance < reward.pointsCost) return false;
      state.pointsBalance -= reward.pointsCost;
      state.redemptions.unshift({
        id: crypto.randomUUID(),
        patientId: patient.id,
        rewardId: reward.id,
        rewardTitle: reward.title,
        pointsCost: reward.pointsCost,
        status: REWARD_STATUSES.REQUESTED,
        requestedAt: new Date().toISOString().slice(0, 10),
        redeemedAt: ""
      });
      state.pointEvents.unshift({
        id: crypto.randomUUID(),
        patientId: patient.id,
        type: "reward_redemption",
        points: -reward.pointsCost,
        createdAt: new Date().toISOString().slice(0, 10),
        description: `Reward requested: ${reward.title}`
      });
      save();
      return true;
    }

    function tierLabel(tier) {
      return ({
        [REWARD_TIERS.SMALL_PERK]: "Small Perk",
        [REWARD_TIERS.LEANER_CREDIT]: "Leaner Credit",
        [REWARD_TIERS.BIGGER_PERK]: "Bigger Perk",
        [REWARD_TIERS.MAJOR_MOMENTUM_REWARD]: "Major Momentum Reward",
        [REWARD_TIERS.FEATURED_PERK]: "Featured Perk"
      })[tier] || tier;
    }

    function rewardIcon(value) {
      const iconMap = {
        gift: "gift",
        ticket: "gift",
        badge: "star",
        star: "star",
        trophy: "spark",
        sparkles: "spark",
        sparkle: "spark",
        spark: "spark",
        flame: "flame"
      };
      return iconMap[String(value || "").trim()] || "gift";
    }

    function featuredReward() {
      return state.rewards.find(reward => reward.isActive && reward.isFeatured);
    }

    function activeRewardTiers() {
      return state.rewards
        .filter(reward => reward.isActive && reward.tier !== REWARD_TIERS.FEATURED_PERK)
        .sort((a, b) => Number(a.pointsCost) - Number(b.pointsCost));
    }

    function redemptionFor(rewardId) {
      return state.redemptions.find(item => item.rewardId === rewardId);
    }

    function nextReward() {
      const active = activeRewardTiers();
      return active.find(reward => reward.pointsCost > state.pointsBalance) || active[active.length - 1] || state.rewards[0];
    }

    function rewardStatus(reward) {
      const redemption = redemptionFor(reward.id);
      if (redemption) return normalizeRewardStatus(redemption.status);
      return state.pointsBalance >= reward.pointsCost ? REWARD_STATUSES.AVAILABLE : REWARD_STATUSES.LOCKED;
    }

    function weekStartDate(dateValue = new Date()) {
      const date = new Date(dateValue);
      const day = date.getDay();
      const diff = date.getDate() - day;
      const start = new Date(date.setDate(diff));
      return start.toISOString().slice(0, 10);
    }

    function supportLabel(type) {
      return ({ [SUPPORT_TYPES.MEAL_IDEA]: "Meal Ideas", [SUPPORT_TYPES.COACH_TIP]: "Coach Tips", [SUPPORT_TYPES.CLINIC_SUPPORT]: "Clinic Support" })[type];
    }

    function supportField(type) {
      return ({ [SUPPORT_TYPES.MEAL_IDEA]: "mealIdeaId", [SUPPORT_TYPES.COACH_TIP]: "coachTipId", [SUPPORT_TYPES.CLINIC_SUPPORT]: "clinicSupportId" })[type];
    }

    function supportItemById(id, type) {
      return state.supportContent.find(item => item.id === id) || supportFallbacks[type];
    }

    function selectSupportItem(type) {
      const approved = state.supportContent
        .filter(item => item.type === type && item.status === CONTENT_STATUSES.APPROVED)
        .sort((a, b) => (a.timesShown || 0) - (b.timesShown || 0) || String(a.createdAt).localeCompare(String(b.createdAt)));
      return approved[0] || supportFallbacks[type];
    }

    // In this local prototype, creating the weekly support selection also marks the
    // chosen approved items as used so admin "weeks remaining" stays honest.
    function ensureWeeklySupportSelection() {
      const currentWeek = weekStartDate();
      let selection = state.weeklySupportSelections.find(item => item.weekStartDate === currentWeek);
      if (selection) return selection;
      const meal = selectSupportItem(SUPPORT_TYPES.MEAL_IDEA);
      const tip = selectSupportItem(SUPPORT_TYPES.COACH_TIP);
      const clinic = selectSupportItem(SUPPORT_TYPES.CLINIC_SUPPORT);
      selection = {
        weekStartDate: currentWeek,
        mealIdeaId: meal.id,
        coachTipId: tip.id,
        clinicSupportId: clinic.id,
        mode: "auto",
        updatedAt: new Date().toISOString().slice(0, 10)
      };
      [meal, tip, clinic].forEach(item => {
        if (!String(item.id).startsWith("fallback")) {
          item.timesShown = (item.timesShown || 0) + 1;
          item.lastShownAt = currentWeek;
          item.status = CONTENT_STATUSES.USED;
        }
      });
      state.weeklySupportSelections.unshift(selection);
      save();
      return selection;
    }

    function currentWeeklySupport() {
      const selection = ensureWeeklySupportSelection();
      return {
        [SUPPORT_TYPES.MEAL_IDEA]: supportItemById(selection.mealIdeaId, SUPPORT_TYPES.MEAL_IDEA),
        [SUPPORT_TYPES.COACH_TIP]: supportItemById(selection.coachTipId, SUPPORT_TYPES.COACH_TIP),
        [SUPPORT_TYPES.CLINIC_SUPPORT]: supportItemById(selection.clinicSupportId, SUPPORT_TYPES.CLINIC_SUPPORT),
        selection
      };
    }

    function unusedApprovedCount(type) {
      return state.supportContent.filter(item => item.type === type && item.status === CONTENT_STATUSES.APPROVED).length;
    }

    function queueHealth(count) {
      if (count >= 8) return ["Healthy", ""];
      if (count >= 4) return ["Getting Low", "warn"];
      return ["Needs Refill", "danger"];
    }

    function defaultIconForType(type) {
      return ({ [SUPPORT_TYPES.MEAL_IDEA]: "fork", [SUPPORT_TYPES.COACH_TIP]: "tip", [SUPPORT_TYPES.CLINIC_SUPPORT]: "message" })[type] || "spark";
    }

    function normalizeIcon(value, type) {
      const icon = String(value || "").trim();
      const map = {
        apple: "fork",
        bowl: "fork",
        "fork-knife": "fork",
        lightbulb: "tip",
        chat: "message",
        compass: "tip",
        heart: "heart",
        "message-circle": "message",
        headset: "message",
        sparkle: "spark",
        fork: "fork",
        tip: "tip",
        message: "message",
        spark: "spark"
      };
      return map[icon] || defaultIconForType(type);
    }

    function csvTemplate() {
      return [
        CSV_HEADERS.join(","),
        `meal-001,${SUPPORT_TYPES.MEAL_IDEA},Simple Protein Plate,"Choose a lean protein, a fiber-rich side, and something fresh.","Build a simple plate around protein first, then add fiber and something fresh. Keep it easy and repeatable this week.",fork-knife,"high-protein|easy-meal|beginner",${CONTENT_STATUSES.APPROVED}`
      ].join("\n");
    }

    // ================= CSV parsing/validation helpers =================
    function parseCsv(text) {
      const rows = [];
      let row = [];
      let cell = "";
      let inQuotes = false;
      for (let i = 0; i < text.length; i++) {
        const char = text[i];
        const next = text[i + 1];
        if (char === '"' && inQuotes && next === '"') {
          cell += '"';
          i++;
        } else if (char === '"') {
          inQuotes = !inQuotes;
        } else if (char === "," && !inQuotes) {
          row.push(cell);
          cell = "";
        } else if ((char === "\n" || char === "\r") && !inQuotes) {
          if (char === "\r" && next === "\n") i++;
          row.push(cell);
          if (row.some(value => String(value).trim() !== "")) rows.push(row);
          row = [];
          cell = "";
        } else {
          cell += char;
        }
      }
      row.push(cell);
      if (row.some(value => String(value).trim() !== "")) rows.push(row);
      return rows;
    }

    function validateSupportCsv(text) {
      const parsed = parseCsv(text);
      if (!parsed.length) {
        return { totalRows: 0, validRows: [], rows: [], errors: ["The CSV file is empty."] };
      }
      const headers = parsed[0].map(header => String(header).trim());
      const headerOk = CSV_HEADERS.every((header, index) => headers[index] === header) && headers.length === CSV_HEADERS.length;
      if (!headerOk) {
        return { totalRows: Math.max(0, parsed.length - 1), validRows: [], rows: [], errors: [`Header row must be exactly: ${CSV_HEADERS.join(",")}`] };
      }
      const rows = parsed.slice(1).map((values, index) => {
        const data = Object.fromEntries(CSV_HEADERS.map((header, i) => [header, String(values[i] || "").trim()]));
        const rowNumber = index + 2;
        const errors = [];
        const warnings = [];
        if (values.length !== CSV_HEADERS.length) errors.push(`Expected ${CSV_HEADERS.length} columns, found ${values.length}.`);
        if (!data.type) errors.push("type is required.");
        if (!SUPPORT_TYPE_LIST.includes(data.type)) errors.push("type must be meal_idea, coach_tip, or clinic_support.");
        if (!data.title) errors.push("title is required.");
        if (!data.shortDescription) errors.push("shortDescription is required.");
        if (!data.status) errors.push("status is required.");
        if (!SUPPORT_IMPORT_STATUSES.includes(data.status)) errors.push("status must be draft, approved, or archived.");
        if (data.title.length > 80) warnings.push("title is longer than 80 characters.");
        if (data.shortDescription.length > 180) warnings.push("shortDescription is longer than 180 characters.");
        if (data.fullContent.length > 1200) warnings.push("fullContent is longer than 1200 characters.");
        if (!data.fullContent) warnings.push("fullContent is empty.");
        if (data.tags.includes(",")) warnings.push("tags should use pipe-separated values, for example high-protein|easy-meal.");
        const tags = data.tags ? data.tags.split("|").map(tag => tag.trim()).filter(Boolean) : [];
        if (tags.length > 10) warnings.push("tags has more than 10 values.");
        if (!data.id) data.id = `support-${Date.now()}-${index + 1}`;
        if (state.supportContent.some(item => item.id === data.id)) warnings.push("ID already exists and will update the existing item.");
        const item = {
          id: data.id,
          type: data.type,
          title: data.title,
          shortDescription: data.shortDescription,
          fullContent: data.fullContent,
          icon: normalizeIcon(data.icon, data.type),
          tags,
          status: data.status,
          createdAt: new Date().toISOString().slice(0, 10),
          updatedAt: new Date().toISOString().slice(0, 10),
          lastShownAt: "",
          timesShown: 0
        };
        return { rowNumber, data, item, errors, warnings, isValid: errors.length === 0 };
      });
      return {
        totalRows: rows.length,
        validRows: rows.filter(row => row.isValid),
        rows,
        errors: []
      };
    }

    function importValidSupportRows() {
      if (!state.csvImport?.validRows?.length) return;
      state.csvImport.validRows.forEach(row => {
        const existingIndex = state.supportContent.findIndex(item => item.id === row.item.id);
        if (existingIndex >= 0) {
          state.supportContent[existingIndex] = { ...state.supportContent[existingIndex], ...row.item, createdAt: state.supportContent[existingIndex].createdAt || row.item.createdAt, timesShown: state.supportContent[existingIndex].timesShown || 0, lastShownAt: state.supportContent[existingIndex].lastShownAt || "" };
        } else {
          state.supportContent.push(row.item);
        }
      });
      state.csvImport = { ...state.csvImport, imported: true, importedCount: state.csvImport.validRows.length };
      save();
    }

    function downloadCsvTemplate() {
      const blob = new Blob([csvTemplate()], { type: "text/csv;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = "leaner-weekly-support-template.csv";
      document.body.appendChild(link);
      link.click();
      link.remove();
      URL.revokeObjectURL(url);
    }

    function routeTo(route) {
      if (!canAccessRoute(route)) {
        route = state.currentUser ? ROUTES.HOME : ROUTES.LOGIN;
      }
      state.route = route;
      render();
      window.scrollTo({ top: 0, behavior: "smooth" });
    }

    function findWin(id) { return state.wins.find(w => w.id === id); }
    function escapeHtml(v) { return String(v ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); }
    function escapeAttr(v) { return escapeHtml(v).replace(/`/g, "&#96;"); }
    function isUsableImageUrl(value) {
      const url = String(value || "");
      return !!url && url !== "photo-placeholder" && /^(data:image\/|blob:|https?:\/\/)/.test(url);
    }
