// ------------------------------------------------------------------------
//  compute.js - Build the per-hour display rows from the raw API data.
//
//  Pure-ish function: feed in (forecast, airQuality, location, vehicleType,
//  vehicleVent, buildingType) and get back { hourlyRows, days, utcOffsetMs }.
//
//  Open-Meteo with timezone=auto returns local wall-clock strings like
//  "2026-05-13T14:00" - no Z suffix. Two forms are used in each row:
//    - String slices (iso.slice(...)) for display & day grouping
//    - A true UTC Date (dt) for solarElevationDeg (which uses .getUTC*).
// ------------------------------------------------------------------------

import {
  vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
  calcConcreteTempPass, calcVehicleInteriorTemp,
  calcIndoorTempPass, calcManagedIndoorTempPass,
} from './physics.js';
import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel } from './utils.js';
import { UTCI_ENVIRONMENTS } from './config.js';

export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, buildingType, utciEnv }) {
  const env = UTCI_ENVIRONMENTS[utciEnv] ?? UTCI_ENVIRONMENTS.open;
  const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;

  // Build a fast lookup map from the air quality hourly data: ISO string - index.
  // Normalise to "YYYY-MM-DDTHH" (13 chars) so forecast timestamps like
  // "2026-05-17T14:00" match AQ timestamps like "2026-05-17T14".
  const aqTimeMap = {};
  if (airQuality?.hourly?.time) {
    airQuality.hourly.time.forEach((t, i) => { aqTimeMap[t.slice(0, 13)] = i; });
  }
  const getAq = (field, iso) => {
    if (!airQuality?.hourly?.[field]) return null;
    const i = aqTimeMap[iso.slice(0, 13)];
    if (i === undefined) return null;
    return airQuality.hourly[field][i] ?? null;
  };

  const hourlyRows = forecast ? forecast.hourly.time.map((iso, i) => {
    const h = forecast.hourly;
    const Ta   = h.temperature_2m[i];
    const RH   = h.relative_humidity_2m[i];
    const dew  = h.dew_point_2m ? h.dew_point_2m[i] : null;
    const va   = h.wind_speed_10m[i];
    const wd   = h.wind_direction_10m ? h.wind_direction_10m[i] : null;
    const gust = h.wind_gusts_10m    ? h.wind_gusts_10m[i]    : null;
    const dir  = h.direct_radiation[i]    || 0;
    const dif  = h.diffuse_radiation[i]   || 0;
    const glob = h.shortwave_radiation[i] || 0;
    const cc     = h.cloud_cover[i];
    const ccLow  = h.cloud_cover_low  ? h.cloud_cover_low[i]  : null;
    const ccMid  = h.cloud_cover_mid  ? h.cloud_cover_mid[i]  : null;
    const ccHigh = h.cloud_cover_high ? h.cloud_cover_high[i] : null;
    const uv     = h.uv_index ? (h.uv_index[i] || 0) : 0;
    const precip      = h.precipitation[i] || 0;
    const precipProb  = h.precipitation_probability ? (h.precipitation_probability[i] ?? 0) : 0;
    const snow        = h.snowfall[i] || 0;
    const soilT0 = h.soil_temperature_0cm    ? h.soil_temperature_0cm[i]    : null;
    const soilT6 = h.soil_temperature_6cm    ? h.soil_temperature_6cm[i]    : null;
    const soilM  = h.soil_moisture_0_to_1cm  ? h.soil_moisture_0_to_1cm[i]  : null;
    // iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
    // For display we slice the string directly - no Date object needed.
    // For solarElevationDeg (which uses .getUTC* internally) we need the
    // true UTC instant: treat the local time as UTC then subtract the offset.
    // e.g. Brisbane UTC+10: local 14:00 - parse as UTC 14:00 - subtract 10h - UTC 04:00 -
    // iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
    // For solarElevationDeg (which uses .getUTC* internally) we need the
    // true UTC instant: treat the local time as UTC then subtract the offset.
    const dt   = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
    const elev = solarElevationDeg(location.lat, location.lon, dt);
    // Use direct_radiation (beam sunlight) + a fraction of diffuse for concrete.
    // direct_radiation is zero on fully overcast days - far more accurate than
    // shortwave_radiation which can be unreliably high even at 100% cloud cover.
    // Diffuse (scattered light through cloud) contributes ~20% as much heat to
    // a surface as direct beam, so we weight it accordingly.
    const effectiveRad = dir + dif * 0.2;
    // concreteT is now stamped in the two-pass section below (thermal lag).
    const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
    const eh       = vaporPressureHpa(Ta, RH);
    // Apply environment modifier - adjust solar inputs and air temp for shaded environments.
    const TaEnv   = Ta   + env.taOffset;
    const dirEnv  = dir  * env.dirFactor;
    const difEnv  = dif  * env.difFactor;
    const globEnv = glob * env.globFactor;
    const Tmrt     = calcTmrt(TaEnv, dirEnv, difEnv, globEnv, elev);
    const utci     = utciApprox(TaEnv, Tmrt, va, eh);
    const utciAdj = utci + precipPenalty(precip, snow, va);
    // Derived
    const compass = windCompass8(wd);
    const { uvA, uvB } = uvSplit(uv, elev);
    const cloudCat = cloudCategory(cc, ccLow, ccMid, ccHigh);
    // Visibility from the main forecast API (metres - km).
    const visKm        = (() => { const v = h.visibility ? h.visibility[i] : null; return v != null ? v / 1000 : null; })();
    const aqi          = getAq('european_aqi', iso);
    const grassPollen  = getAq('grass_pollen', iso);
    const birchPollen  = getAq('birch_pollen', iso);
    const alderPollen  = getAq('alder_pollen', iso);
    const mugwortPollen= getAq('mugwort_pollen', iso);
    const olivePollen  = getAq('olive_pollen', iso);
    const ragweedPollen= getAq('ragweed_pollen', iso);
    // -- FUTURE FEATURE: Activity "What If" Modifier ---------------------------
    // Add two extra columns driven by a user-selected activity level. These are
    // intentionally kept SEPARATE from the core columns above so that baseline
    // profile data stays consistent and comparable across profiles.
    //
    // The user picks an activity from a simple UI picker (no live data needed -
    // this is a forecast/planning tool, not a tracker):
    //   Resting - Walking - Cycling - Running - Sport/Intense
    //
    // Two output columns only (keep it clean):
    //
    //   adjustedSafeTime  - baseline UV safe exposure time - an activity multiplier.
    //                       Higher activity = shorter safe time, because:
    //                         - metabolic heat raises core body temp
    //                         - sweating washes away sunscreen faster
    //                         - more skin blood flow = higher UV sensitivity
    //                       Suggested multipliers (tune with real data):
    //                         Resting: 1.0 (no change)
    //                         Walking: 0.85
    //                         Cycling: 0.75
    //                         Running: 0.60
    //                         Sport:   0.50
    //
    //   heatStressLevel   - a simple label: 'Low' | 'Moderate' | 'High' | 'Very High'
    //                       Derived from UTCI + activity heat load. A runner at
    //                       UTCI 28-C should read 'High' even if a resting person
    //                       would read 'Moderate' at the same UTCI.
    //                       Colour code in the UI: - - - -
    //
    // Implementation sketch:
    //   1. Accept 'activityLevel' as a new param to buildHourlyRows() alongside
    //      vehicleType, buildingType etc.
    //   2. Define ACTIVITY_PRESETS in utils.js (multiplier + utciOffset per level).
    //   3. Compute adjustedSafeTime = baseSafeTime * preset.multiplier
    //   4. Compute heatStressLevel from (utci + preset.utciOffset) banded into labels.
    //   5. Add both fields to the returned row object below.
    //   6. In components.js, render these as optional columns that only appear when
    //      an activity other than 'Resting' is selected - keeps the default table clean.
    // -----------------------------------------------------------------------------

    return {
      iso, dt, Ta, RH, dew, va, wd, gust, dir, dif, glob,
      cc, ccLow, ccMid, ccHigh, cloudCat,
      uv, uvA, uvB,
      precip, precipProb, snow,
      soilT0, soilT6, soilM, vehicleT,
      effectiveRad,
      elev, Tmrt, utci, utciAdj, eh, compass,
      visKm, aqi,
      grassPollen, birchPollen, alderPollen, mugwortPollen, olivePollen, ragweedPollen,
    };
  }) : [];

  // Two-pass calculations: concrete thermal lag + indoor temperature.
  // Both need the full hourly arrays so they can look back at previous
  // hours. Run after hourlyRows is built, then stamp each row.
  if (hourlyRows.length > 0) {
    const TaArr       = hourlyRows.map(r => r.Ta);
    const globArr     = hourlyRows.map(r => r.glob);
    const elevArr     = hourlyRows.map(r => r.elev);
    const radArr      = hourlyRows.map(r => r.effectiveRad);
    const vaArr       = hourlyRows.map(r => r.va);
    const uvArr       = hourlyRows.map(r => r.uv);
    const cloudCatArr = hourlyRows.map(r => r.cloudCat);
    const soilMArr    = hourlyRows.map(r => r.soilM);
    const precipArr   = hourlyRows.map(r => r.precip);
    const snowArr     = hourlyRows.map(r => r.snow);

    // Concrete surface temperature with thermal lag (1.5 h time constant).
    // A slab baking in the sun retains heat when cloud rolls in, and takes
    // a couple of hours of sunshine to fully heat up from a cold start.
    const concreteTemps = calcConcreteTempPass(
      TaArr, radArr, vaArr, elevArr, uvArr, cloudCatArr, soilMArr, precipArr, snowArr
    );
    hourlyRows.forEach((r, i) => { r.concreteT = concreteTemps[i]; });

    const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
    const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
    hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
  }

  // Group those hourly rows into days for the day tabs.
  const days = [];
  hourlyRows.forEach(row => {
    const key = row.iso.slice(0, 10);
    let day = days.find(d => d.key === key);
    if (!day) { day = { key, rows: [] }; days.push(day); }
    day.rows.push(row);
  });

  // -- FUTURE FEATURE v2: Today Summary + Alert System --------------------------
  // After grouping rows into days, generate a per-day summary object that powers
  // a stylish "Today at a Glance" panel shown above or below the main dial.
  //
  // The summary is NOT a live alert/push system - it's a forecast digest that
  // refreshes with the forecast data. Think of it as a smart briefing card.
  //
  // WHAT TO COMPUTE (per day, from that day's rows):
  //   - Peak UTCI+P and time it occurs        - heat stress headline
  //   - Min UTCI+P and time                   - cold stress headline
  //   - Peak UV index and time                - UV warning
  //   - Max precipitation rate and time       - rain/ice warning
  //   - Max vehicle cabin temp                - "dangerous to leave pets/children in car"
  //   - Max pollen level + type               - pollen advisory
  //   - Road condition risk (low air temp + precip - ice risk)
  //
  // ALERT CATEGORIES (each generates a styled warning card if threshold exceeded):
  //   --  Heat warning      UTCI+P > 32-C
  //   -  Cold warning       UTCI+P < 0-C
  //   --  UV warning         UV index > 6
  //   --  Heavy rain         precip > 4mm/h
  //   -  Ice/road risk      Ta < 3-C + any precip (or recent precip overnight)
  //   -  Vehicle danger     vehicleT > 35-C ("don't leave pets or children in car")
  //   -  High pollen        any pollen type > 50 grains/m-
  //
  // DESIGN NOTES:
  //   - Cards should be concise - one line of bold text + a short explanation
  //   - Colour-coded to match the existing UTCI stress band palette
  //   - Collapsible - show top 2-3 alerts by default, expand for full list
  //   - For today only (days[0]); optionally extend to day tabs in a later pass
  //   - The "X-C above seasonal norm" historical context line (see app.js comment)
  //     could live here too, as a subtle subheading under the dial temperature
  //
  // Suggested return shape - add to the return value below:
  //   daySummaries: days.map(day => buildDaySummary(day.rows))
  //
  // where buildDaySummary() is a new helper in this file (or a separate
  // summary.js module if it grows large).
  // -----------------------------------------------------------------------------

  return { hourlyRows, days, utcOffsetMs };
}

// ------------------------------------------------------------------------
//  aggregateRows(rows, interval) - Compress a day's hourly rows into
//  multi-hour buckets for the table view (1h / 2h / 3h / 4h).
//
//  Buckets are CLOCK-ALIGNED from midnight, so a 2h view groups 00-01,
//  02-03, ...; 3h groups 00-02, 03-05, ...; etc. Each output row keeps the
//  LANDING (first) hour's positional fields - iso, dt, solar elevation,
//  global radiation, wind direction, sky/cloud category - so the little
//  scope, clock label and wind vane all show the time the column lands on.
//
//  Every other column is aggregated with the rule that fits its meaning:
//    - SUM  : precipitation totals (1mm/h x 3h = 3mm)
//    - MAX  : risk/peak columns (rain %, UV, gusts, cabin/surface/indoor)
//    - MEAN : smooth continuous quantities (air temp, RH, wind, cloud, ...)
//    - FELT : worst-case felt temp - the warmest hour if it reaches >=20C
//             (heat is the concern), otherwise the coldest (cold concern)
//
//  Derived render values (Delta, Burn) are recomputed downstream from the
//  aggregated Air / UTCI / UV, so they stay consistent automatically.
//
//  interval <= 1 returns the input untouched (zero behaviour change).
// ------------------------------------------------------------------------
const AGG_MEAN = [
  'Ta', 'RH', 'dew', 'va', 'dir', 'dif', 'cc', 'ccLow', 'ccMid', 'ccHigh',
  'soilT0', 'soilT6', 'soilM', 'Tmrt', 'eh', 'visKm', 'aqi', 'effectiveRad',
  'grassPollen', 'birchPollen', 'alderPollen', 'mugwortPollen', 'olivePollen', 'ragweedPollen',
];
const AGG_MAX = [
  'precipProb', 'uv', 'uvA', 'uvB', 'gust',
  'vehicleT', 'concreteT', 'indoorT', 'managedT',
];
const AGG_SUM  = ['precip', 'snow'];
const AGG_FELT = ['utci', 'utciAdj'];

export function aggregateRows(rows, interval) {
  if (!rows || rows.length === 0 || !interval || interval <= 1) return rows;

  // Numbers for a field across the group, skipping null / NaN.
  const nums = (group, f) => group.map(r => r[f]).filter(v => v != null && !isNaN(v));
  const mean = (vals) => vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
  const maxV = (vals) => vals.length ? Math.max(...vals) : null;
  const felt = (vals) => {
    if (!vals.length) return null;
    const hi = Math.max(...vals);
    return hi >= 20 ? hi : Math.min(...vals);
  };

  // Split rows into clock-aligned buckets by floor(localHour / interval).
  const buckets = [];
  let cur = null, curKey = null;
  for (const r of rows) {
    const hour = parseInt(r.iso.slice(11, 13), 10);
    const key  = Math.floor(hour / interval);
    if (key !== curKey) { cur = []; buckets.push(cur); curKey = key; }
    cur.push(r);
  }

  return buckets.map(group => {
    // Start from the landing row so positional fields (iso, dt, elev, glob,
    // wd, compass, cloudCat) carry through unchanged, then overwrite the
    // aggregatable columns.
    const out = { ...group[0] };
    AGG_MEAN.forEach(f => { out[f] = mean(nums(group, f)); });
    AGG_MAX.forEach(f  => { out[f] = maxV(nums(group, f)); });
    AGG_SUM.forEach(f  => { out[f] = group.reduce((a, r) => a + (r[f] || 0), 0); });
    AGG_FELT.forEach(f => { out[f] = felt(nums(group, f)); });
    // Bucket span markers used by the table for "now" highlighting and for
    // matching event cell-tags that fall on any hour within the bucket.
    out.isoHours = group.map(r => r.iso.slice(0, 13));
    out.isoEnd   = group[group.length - 1].iso;
    return out;
  });
}

// ------------------------------------------------------------------------
//  computeWhyFeelsLike(row, env) - Break down the felt-temp delta into its
//  contributing factors for the "Why it feels like this" panel.
//
//  Returns contributions in degrees C relative to plain air temperature.
//  Positive = warmer than air temp, negative = cooler.
//
//  Attribution method - sequential isolation using utciApprox:
//    sunAndSky     - Tmrt vs Ta. Mean radiant temp captures net heat from
//                    direct sun, sky scatter, and ground reflection combined.
//    wind          - UTCI with wind vs without (Tmrt = Ta, neutral RH 50%).
//                    Almost always negative - wind cools.
//    humidity      - UTCI at actual vapour pressure vs neutral RH 50%.
//                    Positive when muggy, near-zero when dry.
//    environment   - taOffset from the active UTCI environment modifier.
//                    e.g. urban +2.5, forest -2.0, desert +3.5.
//    precipitation - precipPenalty() - always 0 or negative.
// ------------------------------------------------------------------------
export function computeWhyFeelsLike(row, env) {
  if (!row) return null;
  const { Ta, Tmrt, va, eh, precip, snow } = row;
  const r1 = (v) => Math.round(v * 10) / 10;

  // Radiation - mean radiant temp above or below air temp.
  const sunAndSky = r1(Tmrt - Ta);

  // Wind - UTCI with actual wind vs calm, holding Tmrt = Ta and RH neutral.
  const ehNeutral = vaporPressureHpa(Ta, 50);
  const wind      = r1(utciApprox(Ta, Ta, va, ehNeutral) - utciApprox(Ta, Ta, 0, ehNeutral));

  // Humidity - actual vapour pressure vs neutral RH 50%, no wind or sun.
  const humidity  = r1(utciApprox(Ta, Ta, 0, eh) - utciApprox(Ta, Ta, 0, ehNeutral));

  // Environment modifier - explicit temperature offset from the env config.
  const environment = env ? r1(env.taOffset) : 0;

  // Precipitation soak penalty - negative or zero.
  const precipitation = precipPenalty(precip, snow, va);

  return { sunAndSky, wind, humidity, environment, precipitation };
}

// ------------------------------------------------------------------------
//  computeGlanceSummary(todayRows, profile, variant, skinType) - Build the
//  environment-aware "Today at a Glance" items for the current day.
//
//  Returns an array of { icon, label, value, alert } objects (4-5 items).
//  Content varies by profile and sub-variant so it stays relevant to
//  whatever the user is actually doing.
//
//  Parameters:
//    todayRows  - hourlyRows for the selected day
//    profile    - active profile key e.g. "farming", "vehicle", "home"
//    variant    - active sub-variant key e.g. "running", "beach" (or null)
//    skinType   - Fitzpatrick skin type key for UV burn time
// ------------------------------------------------------------------------
export function computeGlanceSummary(todayRows, profile, variant, skinType) {
  if (!todayRows || todayRows.length === 0) return [];

  const hhmm   = (iso) => iso ? iso.slice(11, 16) : null;
  const dayRows = todayRows.filter(r => r.elev > 0);

  const peakRow = (field) => todayRows.reduce((best, r) =>
    (r[field] != null && (best == null || r[field] > best[field])) ? r : best, null);

  const maxRainProb = Math.max(0, ...todayRows.map(r => r.precipProb ?? 0));

  // Longest run of rows that satisfy a predicate.
  const longestWindow = (rows, cond) => {
    let best = null, cur = null;
    for (const r of rows) {
      if (cond(r)) {
        cur = cur ? { start: cur.start, end: r, len: cur.len + 1 } : { start: r, end: r, len: 1 };
        if (!best || cur.len > best.len) best = { ...cur };
      } else {
        cur = null;
      }
    }
    return best;
  };

  const pollenLabel = (v) => {
    if (v == null || v < 0) return null;
    if (v < 10)  return 'Low';
    if (v < 50)  return 'Moderate';
    if (v < 200) return 'High';
    return 'Very High';
  };

  const aqiLabel = (v) => {
    if (v == null) return null;
    if (v < 20) return 'Good';
    if (v < 40) return 'Fair';
    if (v < 60) return 'Moderate';
    return 'Poor';
  };

  const moistureLabel = (v) => {
    if (v == null) return null;
    if (v < 0.15) return 'Dry';
    if (v < 0.30) return 'Slightly dry';
    if (v < 0.45) return 'Moist';
    return 'Saturated';
  };

  const rainItem = {
    icon: '🌧',
    label: 'Rain risk',
    value: `${Math.round(maxRainProb)}%`,
    alert: maxRainProb >= 60,
  };

  // ── Farming ────────────────────────────────────────────────────────────
  if (profile === 'farming') {
    const fieldWindow = longestWindow(dayRows, r =>
      r.utciAdj >= 8 && r.utciAdj <= 32 && r.precipProb < 30 && r.va < 12
    );
    const soilWarmRow = todayRows.find(r => r.soilT0 != null && r.soilT0 >= 10);
    const soilM       = todayRows.find(r => r.soilM != null)?.soilM ?? null;
    const maxPollen   = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0));

    return [
      {
        icon: '⏱',
        label: 'Best field work',
        value: fieldWindow
          ? `${hhmm(fieldWindow.start.iso)} – ${hhmm(fieldWindow.end.iso)}`
          : 'No suitable window',
        alert: !fieldWindow,
      },
      {
        icon: '🌱',
        label: 'Soil warms to 10°C by',
        value: soilWarmRow ? hhmm(soilWarmRow.iso) : 'Not today',
        alert: !soilWarmRow,
      },
      rainItem,
      {
        icon: '💧',
        label: 'Soil moisture',
        value: moistureLabel(soilM) ?? '-',
        alert: soilM != null && (soilM < 0.10 || soilM > 0.50),
      },
      ...(maxPollen >= 10 ? [{
        icon: '🌿',
        label: 'Grass pollen',
        value: pollenLabel(maxPollen),
        alert: maxPollen >= 50,
      }] : []),
    ];
  }

  // ── Vehicle ────────────────────────────────────────────────────────────
  if (profile === 'vehicle') {
    const peakCabin  = peakRow('vehicleT');
    const dangerFrom = todayRows.find(r => r.vehicleT != null && r.vehicleT >= 35);

    return [
      {
        icon: '🌡',
        label: 'Peak cabin temp',
        value: peakCabin ? `${Math.round(peakCabin.vehicleT)}° at ${hhmm(peakCabin.iso)}` : '-',
        alert: !!(peakCabin && peakCabin.vehicleT >= 35),
      },
      {
        icon: '🧒',
        label: 'Children/pets in car',
        value: dangerFrom ? `Unsafe from ${hhmm(dangerFrom.iso)}` : 'Safe all day',
        alert: !!dangerFrom,
      },
      rainItem,
      {
        icon: '🌤',
        label: 'Best travel comfort',
        value: (() => {
          const best = todayRows.reduce((b, r) =>
            (b == null || r.utciAdj < b.utciAdj) ? r : b, null);
          return best ? `${hhmm(best.iso)} (${Math.round(best.utciAdj)}° felt)` : '-';
        })(),
        alert: false,
      },
    ];
  }

  // ── Home ───────────────────────────────────────────────────────────────
  if (profile === 'home') {
    const peakIndoor  = peakRow('indoorT');
    const peakManaged = peakRow('managedT');
    const ventWindow  = longestWindow(todayRows, r =>
      r.Ta != null && r.indoorT != null && r.Ta < r.indoorT && r.precipProb < 20
    );
    const peakAqi   = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
    const maxPollen = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0));

    return [
      {
        icon: '🌡',
        label: 'Peak indoor (unmanaged)',
        value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-',
        alert: !!(peakIndoor && peakIndoor.indoorT >= 28),
      },
      {
        icon: '🌡',
        label: 'Peak indoor (managed)',
        value: peakManaged ? `${Math.round(peakManaged.managedT)}°` : '-',
        alert: !!(peakManaged && peakManaged.managedT >= 28),
      },
      {
        icon: '🪟',
        label: 'Open windows',
        value: ventWindow
          ? `${hhmm(ventWindow.start.iso)} – ${hhmm(ventWindow.end.iso)}`
          : 'Keep closed',
        alert: false,
      },
      {
        icon: '💨',
        label: 'Air quality',
        value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
        alert: peakAqi >= 60,
      },
      ...(maxPollen >= 10 ? [{
        icon: '🌼',
        label: 'Pollen',
        value: pollenLabel(maxPollen),
        alert: maxPollen >= 50,
      }] : []),
    ];
  }

  // ── Activities - running / cycling ─────────────────────────────────────
  if (variant === 'running' || variant === 'cycling') {
    const coolWindow = longestWindow(dayRows, r =>
      r.utciAdj >= 5 && r.utciAdj <= 22 && r.precipProb < 30
    );
    const peakUvRow  = peakRow('uv');
    const peakAqi    = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
    const maxPollen  = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0));
    const burnMins   = peakUvRow && peakUvRow.uv > 0
      ? burnLabel(sunburnMinutes(peakUvRow.uv, skinType))
      : null;

    return [
      {
        icon: '⏱',
        label: `Best ${variant} window`,
        value: coolWindow
          ? `${hhmm(coolWindow.start.iso)} – ${hhmm(coolWindow.end.iso)}`
          : 'No cool window today',
        alert: !coolWindow,
      },
      rainItem,
      ...(burnMins ? [{
        icon: '☀',
        label: 'UV burn time (peak)',
        value: burnMins,
        alert: !!(peakUvRow && peakUvRow.uv >= 6),
      }] : []),
      {
        icon: '💨',
        label: 'Air quality',
        value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
        alert: peakAqi >= 60,
      },
      ...(maxPollen >= 10 ? [{
        icon: '🌿',
        label: 'Pollen',
        value: pollenLabel(maxPollen),
        alert: maxPollen >= 50,
      }] : []),
    ];
  }

  // ── Outdoors - beach, park, events etc. (and fallback) ─────────────────
  const comfortWindow = longestWindow(dayRows, r =>
    r.utciAdj >= 9 && r.utciAdj <= 26 && r.precipProb < 30
  );
  const peakFelt   = peakRow('utciAdj');
  const peakUvRow  = peakRow('uv');
  const peakAqi    = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
  const burnMins   = peakUvRow && peakUvRow.uv > 0
    ? burnLabel(sunburnMinutes(peakUvRow.uv, skinType))
    : null;

  return [
    {
      icon: '🌤',
      label: 'Comfortable window',
      value: comfortWindow
        ? `${hhmm(comfortWindow.start.iso)} – ${hhmm(comfortWindow.end.iso)}`
        : 'No comfortable window',
      alert: !comfortWindow,
    },
    {
      icon: '🌡',
      label: 'Peak felt temp',
      value: peakFelt ? `${Math.round(peakFelt.utciAdj)}° at ${hhmm(peakFelt.iso)}` : '-',
      alert: !!(peakFelt && (peakFelt.utciAdj >= 32 || peakFelt.utciAdj < 0)),
    },
    ...(burnMins ? [{
      icon: '☀',
      label: 'UV burn time (peak)',
      value: burnMins,
      alert: !!(peakUvRow && peakUvRow.uv >= 6),
    }] : []),
    rainItem,
    {
      icon: '💨',
      label: 'Air quality',
      value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
      alert: peakAqi >= 60,
    },
  ];
}