/* global React, fmtNum, Modal */
const { useState: _useStSens, useRef: _useRefSens } = React;

// =========================================================
// SensorsWidget — live sensor grid from real meteostanice.
// Shows tiles for each available outdoor sensor with day min/max
// and trend arrow vs the previous reading.
// =========================================================

// Outdoor sensor tiles, in display order. VitrNarazy is intentionally absent —
// gust data is folded into the Vitr tile as a paired series. VitrSmer (wind
// direction) doesn't make sense as a sparkline; the dedicated WindWidget
// renders the compass rose instead.
const OUTDOOR_KEYS = ['TeplotaVnejsi','VlhkostVnejsi','TlakRel','Vitr','SrazkyDen','calculatedRain','SlunZareni','UVindex'];
// Expose to widget-config.jsx so the gear popover can render channel toggles.
window.__sensorOutdoorKeys = OUTDOOR_KEYS;

// Outdoor keys treated as "the meteorology trio" — everything else numeric in
// `latest` is treated as an indoor/auxiliary sensor and discovered dynamically.
const OUTDOOR_ALL = new Set([
  ...OUTDOOR_KEYS, 'VitrNarazy', 'VitrSmer',
]);
const SYSTEM_KEYS = new Set([
  'Datum','LokalitaStanice','KlicApi','Lokalita','Webkamera',
  'DoplCidlaJson','WebKameraURL','WebkameraURL','KameraURL',
  'Webcam','WebCam','Zprava','IdLokality','IdStanice',
]);

// Walk station.latest and return every numeric key that isn't an outdoor /
// system field. This catches Te4, Te5, Vl2, … that aren't in the static FIELDS
// dict so users with extra channels still see all their sensors.
function discoverIndoorKeys(station) {
  if (!station?.latest) return [];
  return Object.keys(station.latest)
    .filter(k => !OUTDOOR_ALL.has(k) && !SYSTEM_KEYS.has(k))
    .filter(k => {
      const v = station.latest[k];
      if (v == null || v === '') return false;
      const n = Number(v);
      return !isNaN(n);
    });
}

// Default metadata for indoor keys not present in station.fields. Pattern-
// matches the channel naming used by the upstream API so Te4 / Vl2 / etc. get
// reasonable labels even when DoplCidlaJson doesn't rename them.
function inferField(key) {
  if (/^Te\d+$/.test(key))  return { label: 'Teplota ' + key.slice(2), unit: '°C', precision: 1, color: '#ef4444' };
  if (/^Vl\d+$/.test(key))  return { label: 'Vlhkost ' + key.slice(2), unit: '%',  precision: 0, color: '#22c55e' };
  if (key === 'TeVody')     return { label: 'Teplota vody',  unit: '°C',    precision: 1, color: '#0ea5e9' };
  if (key === 'VlP')        return { label: 'Vlhkost půdní', unit: '%',     precision: 0, color: '#3b82f6' };
  if (key === 'Co2')        return { label: 'CO₂',           unit: 'ppm',   precision: 0, color: '#ef4444' };
  if (/^Pm/.test(key))      return { label: 'PM ' + key.slice(2).replace(/^(\d)(\d)/, '$1.$2'), unit: 'μg/m³', precision: 1, color: '#f43f5e' };
  if (key === 'TeplotaVnitrni') return { label: 'Teplota vnitřní', unit: '°C', precision: 1, color: '#af4444' };
  if (key === 'VlhkostVnitrni') return { label: 'Vlhkost vnitřní', unit: '%',  precision: 0, color: '#02c55e' };
  return { label: key, unit: '', precision: 1, color: 'var(--accent)' };
}

function getField(station, k) {
  const f = (station?.fields && station.fields[k]) || inferField(k);
  // „Akcent i v grafech a přístrojích“ (shared with the station page).
  return window.__accentCharts ? { ...f, color: 'var(--accent)' } : f;
}
window.__sensorGetField = getField;
window.__sensorDiscoverIndoor = discoverIndoorKeys;

// Bootstrap-icon name for a sensor tile. Outdoor channels have fixed keys, but
// indoor channels are discovered dynamically (Te4, Vl3, Pm10, …) so a static
// lookup left everything past the hard-coded Te1-3/Vl1 with a meaningless
// `circle` — which reads as a missing icon. Pattern-match like inferField(),
// then fall back on the field's unit so even unknown channels get a glyph.
const SENSOR_ICONS = {
  Vitr:'wind', VlhkostVnejsi:'droplet-half', TlakRel:'arrows-collapse',
  calculatedRain:'cloud-rain', SrazkyDen:'cloud-rain-heavy',
  SlunZareni:'brightness-high', UVindex:'sun', VitrNarazy:'speedometer2',
  TeplotaVnejsi:'thermometer-half', VitrSmer:'compass',
  TeplotaVnitrni:'thermometer', VlhkostVnitrni:'droplet',
  TeVody:'water', VlP:'moisture', Co2:'cloud-haze', Pm2:'cloud-fog2',
};
function sensorIcon(k, field) {
  if (SENSOR_ICONS[k]) return SENSOR_ICONS[k];
  if (/^Te\d+$/.test(k)) return 'thermometer';
  if (/^Vl\d+$/.test(k)) return 'droplet';
  if (/^Pm/.test(k))     return 'cloud-fog2';
  if (/co2/i.test(k))    return 'cloud-haze';
  const u = (field && field.unit) || '';
  if (u === '°C')    return 'thermometer';
  if (u === '%')     return 'droplet';
  if (u === 'hPa')   return 'arrows-collapse';
  if (u === 'ppm')   return 'cloud-haze';
  if (u === 'μg/m³') return 'cloud-fog2';
  return 'activity';
}
window.__sensorIcon = sensorIcon;

function trendArrow(curr, prev) {
  if (curr == null || prev == null) return null;
  const diff = curr - prev;
  if (Math.abs(diff) < 0.01) return { cls: 'flat', icon: 'dash' };
  return diff > 0 ? { cls: 'up', icon: 'arrow-up-short' } : { cls: 'down', icon: 'arrow-down-short' };
}

function fmt(v, p=0) {
  if (v == null || isNaN(v)) return '—';
  // toFixed keeps trailing zeros — "12.0 °C" instead of "12 °C" when the
  // station reports a whole-degree reading.
  return window.czNum(v, p);
}

function SensorTile({ k, station, fillArea = true, showMinMax = true, sensorConfig = {} }) {
  const [open, setOpen] = _useStSens(false);
  const field = getField(station, k);
  const latest = station.latest || {};
  const prev = station.prev || {};
  const rawVal = latest[k];
  if (rawVal == null) return null;
  // Vítr je jediná uživatelsky volitelná jednotka — Vitr/VitrNarazy projdou
  // přes Units.wind a do labelu se vloží windLabel (m/s · km/h · mph · uzly).
  const isWind = (k === 'Vitr' || k === 'VitrNarazy');
  const conv = (v) => v == null ? null
    : (isWind && window.Units ? window.Units.wind(v) : v);
  const unitLabel = isWind && window.Units ? window.Units.windLabel() : field.unit;
  const val = conv(rawVal);
  const rawRange = station.dayMinMax ? station.dayMinMax(k) : { min: null, max: null };
  const range = { min: conv(rawRange.min), max: conv(rawRange.max) };
  const trend = trendArrow(rawVal, prev[k]);
  // Pro vítr ukazujeme i MIN/MAX nárazů (VitrNarazy) spárované s průměrem,
  // stejně jako se nárazy párují do hlavní hodnoty pod tímto.
  const rawGustRange = (k === 'Vitr' && station.dayMinMax) ? station.dayMinMax('VitrNarazy') : null;
  const gustRange = rawGustRange
    ? { min: conv(rawGustRange.min), max: conv(rawGustRange.max) }
    : null;
  const showSpark = sensorConfig.showSpark !== false;
  const showRange = sensorConfig.showRange !== false;
  const showTrend = sensorConfig.showTrend !== false;
  // Denní srážky (SrazkyDen) jsou kumulativní denní úhrn — během dne jen
  // roste a o půlnoci se resetuje. MIN/MAX i 24h graf na tom nedávají smysl
  // (MIN by byl skoro vždy 0, graf jen pila resetů), takže pro tento senzor
  // schováme rozsah i sparkline a tile není klikací (žádný detailní graf).
  const isCumulativeRain = (k === 'SrazkyDen');

  // Wind tile pairs gust readings on the same chart — same physical sensor,
  // it would be wasteful to give it its own tile.
  const pair = (k === 'Vitr' && latest.VitrNarazy != null)
    ? { key: 'VitrNarazy', label: 'náraz', color: '#0ea5e9', precision: 1 }
    : null;
  const gustVal = pair ? conv(latest[pair.key]) : null;

  // last 24h: filter station.raw by timestamp
  const cutoff = Date.now() - 24 * 3600 * 1000;
  const points24h = (station.raw || []).filter(r => {
    const t = r.Datum ? window.StationData.parseDatum(r.Datum).getTime() : 0;
    return t >= cutoff && r[k] != null && !isNaN(r[k]);
  });
  const series = points24h.map(r => r[k]);
  const seriesPair = pair ? points24h.map(r => r[pair.key]).filter(v => v != null && !isNaN(v)) : null;

  const body = (
    <>
      <div className="sensor-head">
        <i className={`bi bi-${sensorIcon(k, field)} sensor-icon`} style={{color: field.color}}></i>
        <span className="sensor-label">{field.label}</span>
        {showTrend && trend && <i className={`bi bi-${trend.icon} trend-${trend.cls}`}></i>}
      </div>
      <div className="sensor-value">
        {fmt(val, field.precision)}{pair && gustVal != null
          ? <span className="sensor-pair" title="nárazy">/{fmt(gustVal, pair.precision)}</span>
          : null}<small>{unitLabel}</small>
      </div>
      {/* Rozsah ukazujeme i když MIN === MAX — vynechání celého řádku jinak
          rozbíjelo výšku dlaždice oproti sousedům v gridu. */}
      {!isCumulativeRain && showRange && range.min != null && range.max != null && (
        <div className="sensor-range">
          <span>MIN <strong>{fmt(range.min, field.precision)}{pair && gustRange && gustRange.min != null
            ? <span className="sensor-pair">/{fmt(gustRange.min, pair.precision)}</span> : null}</strong></span>
          <span>MAX <strong>{fmt(range.max, field.precision)}{pair && gustRange && gustRange.max != null
            ? <span className="sensor-pair">/{fmt(gustRange.max, pair.precision)}</span> : null}</strong></span>
        </div>
      )}
      {!isCumulativeRain && showSpark && series.length >= 3 && (
        <MiniSpark series={series} color={field.color} seriesB={seriesPair} colorB={pair?.color} isPct={field.unit === '%'} fillArea={fillArea}/>
      )}
    </>
  );

  // SrazkyDen: bez detailního grafu → tile jen zobrazuje hodnotu, neklikací.
  // sensor-tile-flat: stejný (bezrámečkový, průhledný) vzhled jako klikací
  // dlaždice, které ho dostávají přes `all: unset` z .sensor-tile-btn.
  if (isCumulativeRain) {
    return <div className="sensor-tile sensor-tile-flat" title={field.label}>{body}</div>;
  }

  return (
    <button type="button" className="sensor-tile sensor-tile-btn" onClick={()=>setOpen(true)}
            title={`Detail · ${field.label}`}>
      {body}
      {open && window.Modal && (
        <span onClick={(e)=>e.stopPropagation()}>
        <window.Modal title={`${field.label}${pair ? ' + nárazy' : ''} · 24 h`} onClose={()=>setOpen(false)} wide>
          <BigSpark points={points24h} k={k} field={field} pair={pair} fillArea={fillArea} showMinMax={showMinMax}
                    conv={conv} unitLabel={unitLabel}/>
        </window.Modal>
        </span>
      )}
    </button>
  );
}

function BigSpark({ points, k, field, pair, fillArea = true, showMinMax = true, conv, unitLabel }) {
  if (!points.length) return <div className="muted">Žádná data za posledních 24 h.</div>;
  const W = 900, H = 320, padL = 50, padR = 20, padT = 24, padB = 36;
  const iw = W - padL - padR, ih = H - padT - padB;
  // Pokud SensorTile předal conv (např. m/s → km/h pro vítr), aplikujeme ho na
  // všechny hodnoty před výpočtem osy/extrémů, aby graf zobrazoval převedené
  // jednotky konzistentně se sensor tile.
  const cv = conv || ((x) => x);
  const uLabel = unitLabel != null ? unitLabel : field.unit;
  const vals = points.map(p => cv(p[k]));
  // Paired series (e.g. wind gusts) shares the y-axis so the user can compare
  // amplitudes directly. Filter same way as primary — only valid numerics.
  const pairKey = pair?.key;
  const valsB = pairKey ? points.map(p => cv(p[pairKey])).filter(v => v != null && !isNaN(v)) : [];
  const allVals = valsB.length ? vals.concat(valsB) : vals;
  // Percentage sensors (humidity, soil moisture, cloud cover) read at a glance
  // only when the 0-100 reference is visible — otherwise a 45-50 % swing looks
  // dramatic when in reality it's a small change.
  const isPct = field.unit === '%';
  const vmin = isPct ? 0   : Math.min(...allVals);
  const vmax = isPct ? 100 : Math.max(...allVals);
  // True data extremes — kept separate from the axis range so a humidity chart
  // can still highlight the actual high/low of the day against the 0-100 frame.
  const dataMin = Math.min(...allVals), dataMax = Math.max(...allVals);
  const span = Math.max(1e-6, vmax - vmin);
  const t0 = window.StationData.parseDatum(points[0].Datum).getTime();
  const tN = window.StationData.parseDatum(points[points.length-1].Datum).getTime();
  const x = t => padL + ((t - t0) / Math.max(1, tN - t0)) * iw;
  const y = v => padT + ih - ((v - vmin) / span) * ih;
  const pts = points.map(p => [x(window.StationData.parseDatum(p.Datum).getTime()), y(cv(p[k]))]);
  const ptsB = pairKey ? points
    .filter(p => p[pairKey] != null && !isNaN(p[pairKey]))
    .map(p => [x(window.StationData.parseDatum(p.Datum).getTime()), y(cv(p[pairKey]))]) : null;
  const minIdx = vals.indexOf(dataMin), maxIdx = vals.indexOf(dataMax);
  // Catmull-Rom smoothing via shared helper — natural tangents between
  // neighbouring points instead of the old horizontal-tangent midpoint Bezier.
  const path = window.smoothPath(pts);
  const pathB = ptsB && ptsB.length >= 2 ? window.smoothPath(ptsB) : null;
  const area = path + ` L ${pts[pts.length-1][0]} ${padT+ih} L ${pts[0][0]} ${padT+ih} Z`;
  const ticks = [];
  const stepV = Math.max(0.01, (vmax - vmin) / 4);
  for (let i = 0; i <= 4; i++) ticks.push(vmin + i * stepV);
  const gid = 'bsg-' + k;

  // Hover handler — map wrapper's CSS pixel X to the SVG coordinate space
  // and pick the closest sample by minimising |x(point) - svgX|. Points are
  // time-spaced, so a linear "round" against length wouldn't always pick
  // the visually-nearest one.
  // time ticks every 6h
  const timeTicks = [];
  const start = new Date(t0); start.setMinutes(0,0,0);
  for (let t = start.getTime(); t <= tN; t += 6*3600*1000) {
    if (t >= t0) timeTicks.push(t);
  }
  return (
    <div className="bigspark">
      <div style={{ position: 'relative' }}>
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet" style={{width:'100%',height:'auto',display:'block'}}>
        <defs>
          <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor={field.color} stopOpacity="0.45"/>
            <stop offset="100%" stopColor={field.color} stopOpacity="0.02"/>
          </linearGradient>
        </defs>
        {ticks.map((v, i) => (
          <g key={i}>
            <line x1={padL} x2={W-padR} y1={y(v)} y2={y(v)} stroke="var(--border)" strokeDasharray="2 4"/>
            <text x={padL-6} y={y(v)+4} fontSize="11" textAnchor="end" fill="var(--muted)" fontFamily="var(--mono)">
              {fmt(v, field.precision)}{uLabel}
            </text>
          </g>
        ))}
        {timeTicks.map((t, i) => (
          <text key={i} x={x(t)} y={H-12} fontSize="11" textAnchor="middle" fill="var(--muted)" fontFamily="var(--mono)">
            {new Date(t).getHours().toString().padStart(2,'0')}:00
          </text>
        ))}
        {fillArea && <path d={area} fill={`url(#${gid})`}/>}
        <path d={path} fill="none" stroke={field.color} strokeWidth="2"/>
        {pathB && <path d={pathB} fill="none" stroke={pair.color || '#0ea5e9'} strokeWidth="1.8" strokeDasharray="5 3" opacity="0.9"/>}
        {/* MIN/MAX accents — pinned to the real data extremes, not the axis. */}
        {showMinMax && <>
          <line x1={padL} x2={W-padR} y1={y(dataMax)} y2={y(dataMax)} stroke="#fb923c" strokeDasharray="4 4" opacity="0.6"/>
          <line x1={padL} x2={W-padR} y1={y(dataMin)} y2={y(dataMin)} stroke="#38bdf8" strokeDasharray="4 4" opacity="0.6"/>
          <text x={W-padR-4} y={y(dataMax)-4} fontSize="11" textAnchor="end" fontWeight="700" fill="#fb923c">MAX {fmt(dataMax, field.precision)}{uLabel}</text>
          <text x={W-padR-4} y={y(dataMin)+13} fontSize="11" textAnchor="end" fontWeight="700" fill="#38bdf8">MIN {fmt(dataMin, field.precision)}{uLabel}</text>
          {minIdx >= 0 && <circle cx={pts[minIdx][0]} cy={pts[minIdx][1]} r="4" fill="#38bdf8" stroke="var(--bg)" strokeWidth="1.8"/>}
          {maxIdx >= 0 && <circle cx={pts[maxIdx][0]} cy={pts[maxIdx][1]} r="4" fill="#fb923c" stroke="var(--bg)" strokeWidth="1.8"/>}
        </>}
      </svg>
      {/* hover = own layer: moving the pointer never re-renders the chart */}
      <window.ChartHover vb={{ w: W, h: H }} top={padT} bottom={padT + ih}
        xs={pts.map(p => p[0])}
        marks={(i) => [
          { y: pts[i][1], color: field.color },
          pairKey && points[i][pairKey] != null && !isNaN(points[i][pairKey]) && { y: y(cv(points[i][pairKey])), color: pair.color || '#0ea5e9' },
        ]}
        tip={(i) => {
          const hp = points[i];
          const ht = window.StationData.parseDatum(hp.Datum);
          return (<>
            <div className="tip-time">{window.fmtTipTime(ht)}</div>
            <div className="tip-row"><span className="k"><i className="tip-sw" style={{ background: field.color }} />{field.label}</span><strong>{fmt(cv(hp[k]), field.precision)}{uLabel}</strong></div>
            {pair && hp[pair.key] != null && (
              <div className="tip-row"><span className="k"><i className="tip-sw" style={{ background: pair.color || '#0ea5e9' }} />{pair.label}</span><strong>{fmt(cv(hp[pair.key]), pair.precision)}{uLabel}</strong></div>
            )}
          </>);
        }} />
      </div>
      <div style={{display:'flex',gap:18,justifyContent:'center',alignItems:'center',marginTop:14,fontFamily:'var(--mono)',fontSize:13,flexWrap:'wrap'}}>
        <span style={{display:'inline-flex',alignItems:'center',gap:6}}>
          <span style={{width:14,height:2,background:field.color,borderRadius:1}}></span>
          {field.label}
        </span>
        {pair && (
          <span style={{display:'inline-flex',alignItems:'center',gap:6}}>
            <span style={{width:14,height:2,background:pair.color || '#0ea5e9',borderRadius:1,backgroundImage:'repeating-linear-gradient(90deg,currentColor 0 4px,transparent 4px 7px)',color:pair.color||'#0ea5e9'}}></span>
            {pair.label}
          </span>
        )}
        <span style={{opacity:0.4}}>·</span>
        <span>MIN <strong>{fmt(dataMin, field.precision)}{uLabel}</strong></span>
        <span>MAX <strong>{fmt(dataMax, field.precision)}{uLabel}</strong></span>
        <span>AKT <strong>{fmt(vals[vals.length-1], field.precision)}{uLabel}</strong></span>
        <span style={{opacity:0.6}}>{points.length}× / 24 h</span>
      </div>
    </div>
  );
}

function MiniSpark({ series, color, seriesB, colorB, isPct, fillArea = true }) {
  const W = 200, H = 36;
  // Shared y-axis so wind and gust live on the same scale — gust is always
  // ≥ wind so the gust line traces visibly above the wind line. Percentage
  // sensors use the full 0-100 frame so a 45-50 % swing reads as small.
  const all = seriesB && seriesB.length ? series.concat(seriesB) : series;
  const vmin = isPct ? 0   : Math.min(...all);
  const vmax = isPct ? 100 : Math.max(...all);
  const span = Math.max(1e-6, vmax - vmin);
  const buildPath = (data) => {
    const xi = i => (i / Math.max(1, data.length - 1)) * W;
    const yi = v => H - 2 - ((v - vmin) / span) * (H - 6);
    const pts = data.map((v, i) => [xi(i), yi(v)]);
    return {
      path: window.smoothPath(pts),
      areaClose: ` L ${xi(data.length-1)} ${H} L ${xi(0)} ${H} Z`,
    };
  };
  const main = buildPath(series);
  const pair = seriesB && seriesB.length >= 3 ? buildPath(seriesB) : null;
  const gid = 'msg-' + Math.random().toString(36).slice(2, 8);
  return (
    <svg className="sensor-spark" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
      <defs>
        <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.4"/>
          <stop offset="100%" stopColor={color} stopOpacity="0.02"/>
        </linearGradient>
      </defs>
      {fillArea && <path d={main.path + main.areaClose} fill={`url(#${gid})`}/>}
      {pair && <path d={pair.path} fill="none" stroke={colorB || '#0ea5e9'} strokeWidth="1.2" strokeDasharray="3 2" opacity="0.85"/>}
      <path d={main.path} fill="none" stroke={color} strokeWidth="1.5"/>
    </svg>
  );
}

// Renders the per-sensor toggle strip used in edit mode. `keys` is the list
// of sensors actually available on this station; user toggles which to display.
function SensorConfigStrip({ keys, station, hiddenKeys, onToggleKey }) {
  if (!keys.length) return null;
  return (
    <div className="sensor-config">
      <div className="sensor-config-label"><i className="bi bi-toggles"></i> Zobrazená čidla</div>
      <div className="sensor-config-chips">
        {keys.map(k => {
          const hidden = hiddenKeys.includes(k);
          const label = getField(station, k).label;
          return (
            <button key={k} type="button"
                    className={`sensor-config-chip ${hidden ? 'off' : 'on'}`}
                    onClick={() => onToggleKey(k)}
                    title={hidden ? 'Zobrazit' : 'Skrýt'}>
              <i className={`bi bi-${hidden ? 'square' : 'check2-square'}`}></i>
              <span>{label}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

window.SensorsWidget = function SensorsWidget({ station, editing, hiddenKeys = [], onToggleKey, fillArea, showMinMax, config = {} }) {
  if (!station) {
    return (
      <div className="muted" style={{padding:16, textAlign:'center'}}>
        <i className="bi bi-broadcast-pin" style={{fontSize:'calc(24px * var(--fs))', marginBottom:6, display:'block'}}></i>
        Stanice nedostupná
      </div>
    );
  }
  const latest = station.latest || {};
  const outdoor = OUTDOOR_KEYS.filter(k => latest[k] != null);
  if (!outdoor.length) {
    return <div className="muted">Stanice nehlásí žádné senzory.</div>;
  }
  const visible = outdoor.filter(k => !hiddenKeys.includes(k));
  return (
    <>
      <div className="sensors-grid">
        {visible.length
          ? visible.map(k => <SensorTile key={k} k={k} station={station} fillArea={fillArea} showMinMax={showMinMax} sensorConfig={config}/>)
          : <div className="muted" style={{gridColumn:'1/-1',padding:'12px',textAlign:'center'}}>Všechna čidla jsou skrytá. {editing ? 'Zapněte je výše.' : 'Zapněte v režimu Upravit.'}</div>}
      </div>
    </>
  );
};

window.IndoorWidget = function IndoorWidget({ station, editing, hiddenKeys = [], onToggleKey, fillArea, showMinMax, config = {} }) {
  if (!station) return <div className="muted">Stanice nedostupná</div>;
  // Indoor list is discovered from `station.latest` so extra channels (Te4,
  // Te5, Vl2, …) show up automatically without hard-coding every variant.
  const indoor = discoverIndoorKeys(station);
  if (!indoor.length) {
    return (
      <div className="muted" style={{padding:16, textAlign:'center'}}>
        <i className="bi bi-house" style={{fontSize:'calc(22px * var(--fs))', marginBottom:6, display:'block'}}></i>
        Žádné vnitřní senzory.
      </div>
    );
  }
  const visible = indoor.filter(k => !hiddenKeys.includes(k));
  return (
    <>
      <div className="sensors-grid">
        {visible.length
          ? visible.map(k => <SensorTile key={k} k={k} station={station} fillArea={fillArea} showMinMax={showMinMax} sensorConfig={config}/>)
          : <div className="muted" style={{gridColumn:'1/-1',padding:'12px',textAlign:'center'}}>Všechna čidla jsou skrytá. {editing ? 'Zapněte je výše.' : 'Zapněte v režimu Upravit.'}</div>}
      </div>
    </>
  );
};
