/* global React, WeatherData, fmtNum */
const { useMemo, useState, useRef, useEffect } = React;

// ─────────────────────────────────────────────────────────────────────
// WebcamPlayer — detail kamery: posledních 24 h snímků (API
// /app/?webcam_id=X, po 10 min) jako časosběr: přehrát / pozastavit, krok,
// rychlost, posuvník s hodinami, „Živě“ = nejnovější snímek, „Video 24 h“ =
// MP4 ze serveru (/video/?priprav=1&id=X → odkaz ke stažení). Snímky se
// načítají až podle potřeby (~110 kB); nový se ukáže, až je načtený, takže
// obraz nebliká. Klávesy: mezerník, ← →, Home, End.
// Stejná komponenta je na předpovědi (extras2.jsx) i na stanici (app.jsx).
// ─────────────────────────────────────────────────────────────────────
const WTL_API = '/app/'; // stejný server (absolutní adresa padala na CORS z kamerypocasi.cz)
const WTL_DAYS = ['Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So'];
function wtlStamp(d) {
  return `${WTL_DAYS[d.getDay()]} ${d.getDate()}. ${d.getMonth() + 1}. ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
}
window.WebcamPlayer = function WebcamPlayer({ webcamId, fallbackUrl }) {
  const [frames, setFrames] = React.useState(null);
  const [failed, setFailed] = React.useState(false);
  const [idx, setIdx] = React.useState(0);
  const [shown, setShown] = React.useState(fallbackUrl || null);
  const [playing, setPlaying] = React.useState(false);
  const [speed, setSpeed] = React.useState(2);
  const [video, setVideo] = React.useState('idle');     // idle | busy | err
  const cache = React.useRef(new Map());
  const playRef = React.useRef(false);
  playRef.current = playing;

  React.useEffect(() => {
    let alive = true;
    fetch(`${WTL_API}?webcam_id=${encodeURIComponent(webcamId)}&_=${Date.now()}`, { cache: 'no-store' })
      .then(r => r.json())
      .then(j => {
        if (!alive) return;
        const list = (j.webkamera_snimky || []).filter(x => x && x.url && x.snimek_cas)
          .map(x => ({ t: new Date(x.snimek_cas * 1000), url: x.url })).sort((a, b) => a.t - b.t);
        if (!list.length) { setFailed(true); return; }
        setFrames(list); setIdx(list.length - 1);
      })
      .catch(() => alive && setFailed(true));
    return () => { alive = false; };
  }, [webcamId]);

  const get = (url) => {
    const c = cache.current;
    if (!c.has(url)) c.set(url, new Promise((res) => {
      const im = new Image();
      im.decoding = 'async';
      im.onload = () => res(true); im.onerror = () => res(false);
      im.src = url;
    }));
    return c.get(url);
  };
  const [ready, setReady] = React.useState(true);
  React.useEffect(() => {
    if (!frames) return undefined;
    let alive = true;
    const url = frames[idx].url;
    setReady(false);
    get(url).then(() => { if (alive) { setShown(url); setReady(true); } });
    return () => { alive = false; };
  }, [idx, frames]);

  // playback: next frame only once it is loaded; stops on the newest one
  React.useEffect(() => {
    if (!playing || !frames) return undefined;
    for (let k = 1; k <= 10 && idx + k < frames.length; k++) get(frames[idx + k].url);
    if (idx >= frames.length - 1) { setPlaying(false); return undefined; }
    const t = setTimeout(() => {
      get(frames[idx + 1].url).then(() => { if (playRef.current) setIdx(i => Math.min(i + 1, frames.length - 1)); });
    }, 1000 / (5 * speed));
    return () => clearTimeout(t);
  }, [playing, idx, speed, frames]);

  const n = frames ? frames.length : 0;
  const toggle = () => {
    if (!frames) return;
    if (!playing && idx >= n - 1) setIdx(0);
    setPlaying(p => !p);
  };
  const step = (d) => { setPlaying(false); setIdx(i => Math.max(0, Math.min(n - 1, i + d))); };
  React.useEffect(() => {
    const onKey = (e) => {
      if (!frames || /INPUT|TEXTAREA|SELECT|BUTTON/.test(e.target.tagName)) return;   // posuvník a tlačítka mají klávesy své
      if (e.key === ' ') { e.preventDefault(); toggle(); }
      else if (e.key === 'ArrowLeft') { e.preventDefault(); step(-1); }
      else if (e.key === 'ArrowRight') { e.preventDefault(); step(1); }
      else if (e.key === 'Home') { e.preventDefault(); step(-n); }
      else if (e.key === 'End') { e.preventDefault(); step(n); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  });

  const download = async () => {
    if (video === 'busy') return;
    setVideo('busy');
    try {
      const r = await fetch(`/video/?priprav=1&id=${encodeURIComponent(webcamId)}`, { cache: 'no-store' });
      const j = await r.json();
      if (!r.ok || !j.url) throw new Error(j.chyba || r.status);
      const a = document.createElement('a');
      a.href = j.url; a.download = j.soubor || '';
      document.body.appendChild(a); a.click(); a.remove();
      setVideo('idle');
    } catch (e) {
      setVideo('err');
      setTimeout(() => setVideo('idle'), 4000);
    }
  };

  const cur = frames ? frames[idx] : null;
  const ticks = [];
  if (frames && n > 1) {
    frames.forEach((f, i) => {
      const h = f.t.getHours(), m = f.t.getMinutes();
      if (m < 10 && h % 3 === 0) ticks.push({ i, label: h === 0 ? WTL_DAYS[f.t.getDay()] : String(h), day: h === 0 });
    });
  }
  return (
    <div className="wtl">
      <div className="wtl-view">
        {shown && <img src={shown} alt="Snímek z webkamery" />}
        {cur && <span className="wtl-stamp">{wtlStamp(cur.t)}{idx === n - 1 ? <b>nejnovější</b> : null}</span>}
        {!ready && <span className="wtl-spin" aria-hidden="true"></span>}
      </div>
      {failed && <div className="wtl-note">Starší snímky se nepodařilo načíst — zobrazuji poslední.</div>}
      {frames && (
        <div className="wtl-bar">
          <div className="wtl-btns">
            <button type="button" className="wtl-b" onClick={() => step(-1)} disabled={idx <= 0} aria-label="O snímek zpět"><i className="bi bi-skip-start-fill"></i></button>
            <button type="button" className="wtl-b wtl-play" onClick={toggle} aria-label={playing ? 'Pozastavit' : 'Přehrát 24 h'}>
              <i className={`bi bi-${playing ? 'pause-fill' : 'play-fill'}`}></i>
            </button>
            <button type="button" className="wtl-b" onClick={() => step(1)} disabled={idx >= n - 1} aria-label="O snímek vpřed"><i className="bi bi-skip-end-fill"></i></button>
          </div>
          <div className="wtl-track">
            <input type="range" min="0" max={n - 1} step="1" value={idx} aria-label="Čas snímku"
                   aria-valuetext={cur ? wtlStamp(cur.t) : ''}
                   style={{ '--p': `${(idx / Math.max(1, n - 1)) * 100}%` }}
                   onChange={(e) => { setPlaying(false); setIdx(Number(e.target.value)); }} />
            <div className="wtl-ticks" aria-hidden="true">
              {ticks.map(t => (
                <span key={t.i} className={t.day ? 'is-day' : ''} style={{ left: `${(t.i / (n - 1)) * 100}%` }}>{t.label}</span>
              ))}
            </div>
          </div>
          <div className="wtl-seg" role="group" aria-label="Rychlost">
            {[1, 2, 4].map(v => (
              <button key={v} type="button" aria-pressed={speed === v} onClick={() => setSpeed(v)}>{v}×</button>
            ))}
          </div>
          <button type="button" className="wtl-b wtl-live" onClick={() => { setPlaying(false); setIdx(n - 1); }} disabled={idx === n - 1}>
            <i className="bi bi-broadcast"></i><span>Živě</span>
          </button>
          <button type="button" className={`wtl-b wtl-video ${video}`} onClick={download} disabled={video === 'busy'}
                  title="Video ze snímků za posledních 24 h (MP4)">
            <i className={`bi bi-${video === 'busy' ? 'hourglass-split' : video === 'err' ? 'exclamation-triangle' : 'download'}`}></i>
            <span>{video === 'busy' ? 'Připravuji video…' : video === 'err' ? 'Nepovedlo se' : 'Video 24 h'}</span>
          </button>
        </div>
      )}
    </div>
  );
};

// Webcam widget — pulls from station.webcamSnapshot (new API shape) or
// falls back to legacy station.config.webcam / SiteConfig.webcam URLs.
// The WHOLE picture, never cropped: it's scaled down into the tile — the frame
// (`.webcam-fit`) gets the picture's own proportions (from the loaded image),
// as big as the tile allows, at the top and horizontally centred; the caption
// sits on the picture; picture + the row under it are centred in the tile, the
// row is 40 px like the radar's controls, so in tiles of the same size the
// webcam picture and the radar map (16:9 frame) are equally big and level.
// Right under it (option showLink): the station's own
// camera (API IdWebcam) → a link to it on kamerypocasi.cz (its detail opens
// from `#kamera=<id>`); no own camera → the API lends a nearby public Windy
// webcam (IdWebcam null, UrlWebcam) → „Kamera z okolí“ (Lukáš: no „Windy“ in the UI) + an invitation
// „Přidejte si vlastní kameru“ to the guide (kamerypocasi.cz root), and the
// caption doesn't claim the station's place. No camera at all → the same
// invitation instead of the picture. Alone in a row the
// tile is as tall as the picture at full width (≤ 420 px, CSS --wc-ar).
// Click → fullscreen modal.
const KP_WEB = 'https://www.kamerypocasi.cz/';
const KP_KAMERA = KP_WEB + 'aktualne/#kamera=';
const WC_LINK_H = 40;                 // row under the picture (px, incl. the 8 px gap) = radar controls
window.WebcamWidget = function WebcamWidget({ station, config = {} }) {
  const [open, setOpen] = React.useState(false);
  // callback ref: the picture often appears only after the station data
  // (placeholder first), a ref measured once at mount would miss it
  const [tileRef, box] = window.useBoxRef();
  const [ar, setAr] = React.useState(16 / 9);
  const camId = station?.webcam?.idWebcam || null;
  const borrowed = !camId && !!station?.webcam?.urlWebcam;   // nearby Windy webcam lent by the API
  const showLink = (!!camId || borrowed) && config.showLink !== false;
  const linkH = showLink ? WC_LINK_H : 0;
  const availH = box ? box.h - linkH : 0;
  const fit = box && box.w > 0 && availH > 0
    ? (box.w / availH > ar ? { width: Math.floor(availH * ar), height: availH } : { width: box.w, height: Math.floor(box.w / ar) })
    : undefined;
  const webcamSnap = station?.webcamSnapshot || null;
  const legacyUrl = !webcamSnap
    ? (station?.config?.webcam || window.SiteConfig?.webcam || null)
    : null;
  const thumb = webcamSnap?.url
    || (legacyUrl ? legacyUrl + (legacyUrl.includes('?') ? '&' : '?') + '_=' + Math.floor(Date.now() / 60000) : null);
  const detail = webcamSnap?.detailUrl || webcamSnap?.url
    || (legacyUrl ? legacyUrl + (legacyUrl.includes('?') ? '&' : '?') + '_=' + Math.floor(Date.now() / 60000) : null);
  const taken = webcamSnap?.snimek_cas ? new Date(webcamSnap.snimek_cas * 1000) : null;
  const locality = station?.locality;
  const showCaption = config.showCaption !== false;

  // No camera at this locality: invite to add one (guide on kamerypocasi.cz).
  if (!thumb) {
    return (
      <div className="webcam-empty webcam-none">
        <i className="bi bi-camera-video" aria-hidden="true"></i>
        <div className="wn-title">Webkamera tu zatím není</div>
        <small>Kamera s výhledem na oblohu se připojí ke kamerypocasi.cz a její snímky se ukážou tady.</small>
        <a className="wn-btn" href={KP_WEB} target="_blank" rel="noopener">
          Přidejte si vlastní kameru<i className="bi bi-box-arrow-up-right" aria-hidden="true"></i>
        </a>
      </div>
    );
  }
  return (
    <>
      <div ref={tileRef} className="webcam-w" style={{ '--wc-ar': ar, '--wc-link': linkH + 'px' }}>
      <button className="webcam-tile" style={fit} onClick={() => setOpen(true)} title="Otevřít webkameru">
        <span className="webcam-fit">
          <img src={thumb} alt="Webkamera"
               onLoad={(e) => { const i = e.currentTarget; if (i.naturalWidth && i.naturalHeight) setAr(i.naturalWidth / i.naturalHeight); }}
               onError={(e) => { e.currentTarget.style.opacity = '0.3'; }} />
          {showCaption && (
            <span className="webcam-caption">
              {(borrowed || locality) && <span className="loc">{borrowed ? 'Kamera z okolí' : locality}</span>}
              {taken && (
                <span className="when">
                  {taken.toLocaleString('cs-CZ', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' })}
                </span>
              )}
            </span>
          )}
        </span>
      </button>
      {showLink && camId && (
        <a className="webcam-link" href={KP_KAMERA + encodeURIComponent(camId)} target="_blank" rel="noopener"
           title="Detail kamery na kamerypocasi.cz: posledních 24 h, video, historie">
          <i className="bi bi-camera-video" aria-hidden="true"></i>Kamera na kamerypocasi.cz
          <i className="bi bi-box-arrow-up-right" aria-hidden="true"></i>
        </a>
      )}
      {showLink && borrowed && (
        <div className="webcam-under">
          {!showCaption && <span className="webcam-src" title="Stanice nemá vlastní kameru — tohle je veřejná kamera z okolí">Kamera z okolí</span>}
          <a className="webcam-link" href={KP_WEB} target="_blank" rel="noopener"
             title="Jak připojit vlastní kameru — kamerypocasi.cz">
            <i className="bi bi-camera-video" aria-hidden="true"></i>Přidejte si vlastní kameru
            <i className="bi bi-box-arrow-up-right" aria-hidden="true"></i>
          </a>
        </div>
      )}
      </div>
      {open && window.Modal && (
        <window.Modal onClose={() => setOpen(false)} kind="webcam" bodyClass="webcam-modal-body"
                      title={<>
                        Webkamera
                        {borrowed ? <span className="chart-unit"> · z okolí</span>
                          : locality ? <span className="chart-unit"> · {locality}</span> : null}
                        {!station?.webcam?.idWebcam && taken ? <span className="chart-unit"> · {taken.toLocaleString('cs-CZ', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' })}</span> : null}
                      </>}>
          {station?.webcam?.idWebcam
            ? <window.WebcamPlayer webcamId={station.webcam.idWebcam} fallbackUrl={detail} />
            : <img src={detail} alt="Webkamera"/>}
        </window.Modal>
      )}
    </>
  );
};

// 24h temperature combined widget — past from station, future from forecast
window.TempSparkWidget = function TempSparkWidget({ station, hourly, compact = false }) {
  const points = useMemo(() => {
    const pts = [];
    const now = Date.now();
    // past: last 24h from station measurements (assume station.measurements [{t, T}])
    const meas = station?.measurements || [];
    for (const m of meas) {
      if (!m.time || m.temp == null) continue;
      const age = (now - m.time.getTime()) / 3600000;
      if (age <= 24 && age >= 0) pts.push({ t: m.time, v: m.temp, kind: 'past' });
    }
    // future: next 24h from forecast hourly
    (hourly || []).slice(0, 24).forEach(h => pts.push({ t: h.time, v: h.temp, kind: 'future' }));
    pts.sort((a, b) => a.t - b.t);
    return pts;
  }, [station, hourly]);

  const [hover, setHover] = useState(null);
  const wrapRef = useRef(null);

  if (!points.length) return <div className="muted">Načítání…</div>;
  // Vyšší viewBox 800×280 (poměr 2.86:1) → graf má víc vertikálního prostoru
  // než původní 800×160. Padding bezpečně izoluje tick labels a x-axis
  // popisky od plochy křivky a od MIN/MAX značek.
  const W = 800, H = 280, padL = 50, padR = 26, padT = 26, padB = 38;
  const iw = W - padL - padR, ih = H - padT - padB;
  const vals = points.map(p => p.v);
  const dataMin = Math.min(...vals), dataMax = Math.max(...vals);
  const vmin = Math.floor(dataMin - 1), vmax = Math.ceil(dataMax + 1);
  const t0 = points[0].t.getTime(), tN = points[points.length - 1].t.getTime();
  const x = t => padL + ((t - t0) / Math.max(1, tN - t0)) * iw;
  const y = v => padT + ih - ((v - vmin) / Math.max(1, vmax - vmin)) * ih;
  const nowX = x(Date.now());
  // Mark the actual min/max points so the eye can find the extremes at a glance.
  const minIdx = vals.indexOf(dataMin), maxIdx = vals.indexOf(dataMax);

  // Catmull-Rom smoothing — see common.jsx smoothPath helper.
  const pts = points.map(p => [x(p.t.getTime()), y(p.v)]);
  const path = window.smoothPath(pts);
  const area = path + ` L ${pts[pts.length-1][0]} ${padT+ih} L ${pts[0][0]} ${padT+ih} Z`;

  // Hover: map mouse X (CSS pixels in the wrapper) to the SVG's coordinate
  // system, then find the nearest datapoint by minimising |x(point) - svgX|.
  // Points are time-indexed (not evenly spaced) so a linear "round" wouldn't
  // be accurate near the past/future seam.
  const onMove = (e) => {
    if (!wrapRef.current || !points.length) return;
    const r = wrapRef.current.getBoundingClientRect();
    if (r.width <= 0) return;
    const svgX = ((e.clientX - r.left) / r.width) * W;
    let bestI = 0, bestDist = Infinity;
    for (let i = 0; i < pts.length; i++) {
      const d = Math.abs(pts[i][0] - svgX);
      if (d < bestDist) { bestDist = d; bestI = i; }
    }
    setHover(bestI);
  };
  const hoverPt = hover != null ? points[hover] : null;

  const tNow = points.find(p => p.kind === 'future')?.v ?? points[points.length-1].v;
  const t24h = points[points.length-1]?.v;
  const t24hAgo = points[0]?.v;
  const trend = tNow - t24hAgo;

  // ticks
  const ticks = [];
  const stepC = Math.max(2, Math.ceil((vmax - vmin) / 4));
  for (let v = vmin; v <= vmax; v += stepC) ticks.push(v);

  return (
    <div className={`spark-widget ${compact ? 'spark-compact' : ''}`}
         ref={wrapRef}
         onMouseMove={onMove}
         onMouseLeave={() => setHover(null)}>
      {!compact && (
      <div className="spark-stats">
        <div className="spark-stat">
          <small>−24 h</small>
          <strong>{fmtNum(t24hAgo, 1)}°</strong>
        </div>
        <div className="spark-stat">
          <small>teď</small>
          <strong className="big">{fmtNum(tNow, 1)}°</strong>
        </div>
        <div className="spark-stat">
          <small>+24 h</small>
          <strong>{fmtNum(t24h, 1)}°</strong>
        </div>
        <div className="spark-stat">
          <small>změna</small>
          <strong className={trend > 0.3 ? 'up' : trend < -0.3 ? 'down' : 'flat'}>
            {trend > 0 ? '+' : ''}{fmtNum(trend, 1)}°
          </strong>
        </div>
      </div>
      )}
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" className="spark-svg">
        <defs>
          <linearGradient id="sparkGrad" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.4"/>
            <stop offset="100%" stopColor="var(--accent)" stopOpacity="0.02"/>
          </linearGradient>
        </defs>
        {ticks.map(v => (
          <g key={v}>
            <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} textAnchor="end" fill="var(--text-muted)" fontFamily="var(--mono)"
                  style={{fontSize:'12px', fontWeight:500}}>{v}°</text>
          </g>
        ))}
        {/* past/future split */}
        <rect x={padL} y={padT} width={Math.max(0, nowX - padL)} height={ih} fill="var(--text)" opacity="0.04"/>
        <path d={area} fill="url(#sparkGrad)"/>
        <path d={path} fill="none" stroke="var(--accent)" strokeWidth="2"/>
        {/* min/max horizontal markers — the actual extremes within the data */}
        <line x1={padL} x2={W-padR} y1={y(dataMax)} y2={y(dataMax)} stroke="#fb923c" strokeDasharray="3 3" opacity="0.7"/>
        <line x1={padL} x2={W-padR} y1={y(dataMin)} y2={y(dataMin)} stroke="#38bdf8" strokeDasharray="3 3" opacity="0.7"/>
        <text x={W-padR-4} y={y(dataMax)-5} textAnchor="end" fill="#fb923c" fontFamily="var(--mono)"
              style={{fontSize:'13px', fontWeight:700}}>MAX {fmtNum(dataMax,1)}°</text>
        <text x={W-padR-4} y={y(dataMin)+18} textAnchor="end" fill="#38bdf8" fontFamily="var(--mono)"
              style={{fontSize:'13px', fontWeight:700}}>MIN {fmtNum(dataMin,1)}°</text>
        {minIdx >= 0 && <circle cx={pts[minIdx][0]} cy={pts[minIdx][1]} r="4.5" fill="#38bdf8" stroke="var(--bg)" strokeWidth="2"/>}
        {maxIdx >= 0 && <circle cx={pts[maxIdx][0]} cy={pts[maxIdx][1]} r="4.5" fill="#fb923c" stroke="var(--bg)" strokeWidth="2"/>}
        {/* now marker */}
        <line x1={nowX} x2={nowX} y1={padT} y2={padT+ih} stroke="var(--accent)" strokeDasharray="2 3"/>
        <text x={nowX} y={padT-6} textAnchor="middle" fill="var(--accent)" fontFamily="var(--mono)" letterSpacing="0.08em"
              style={{fontSize:'11px', fontWeight:700}}>TEĎ</text>
        {/* hover indicator */}
        {hover != null && pts[hover] && (
          <g>
            <line x1={pts[hover][0]} x2={pts[hover][0]} y1={padT} y2={padT+ih}
                  stroke="var(--text)" strokeOpacity="0.55" strokeDasharray="3 2"/>
            <circle cx={pts[hover][0]} cy={pts[hover][1]} r="4.5"
                    fill="var(--accent)" stroke="var(--bg)" strokeWidth="2"/>
          </g>
        )}
        {/* x labels */}
        <text x={padL} y={H-12} fill="var(--text-muted)" fontFamily="var(--mono)"
              style={{fontSize:'11px', fontWeight:500}}>−24 h</text>
        <text x={W-padR} y={H-12} textAnchor="end" fill="var(--text-muted)" fontFamily="var(--mono)"
              style={{fontSize:'11px', fontWeight:500}}>+24 h</text>
      </svg>
      {hoverPt && (
        <div className="chart-tip" style={{
          // Clamp so the tooltip's translateX(-50%) centred box doesn't run
          // off the wrapper edges when hovering near 0 % or 100 %.
          left: Math.max(12, Math.min(88, (pts[hover][0] / W) * 100)) + '%',
          top: 4,
        }}>
          <div className="tip-time">
            {WeatherData.WD_SHORT[hoverPt.t.getDay()]} {String(hoverPt.t.getHours()).padStart(2,'0')}:{String(hoverPt.t.getMinutes()).padStart(2,'0')}
            <small style={{marginLeft:6, opacity:0.7, fontWeight:400}}>
              {hoverPt.kind === 'past' ? 'stanice' : 'předpověď'}
            </small>
          </div>
          <div className="tip-row">
            <span className="k">Teplota</span>
            <strong>{fmtNum(hoverPt.v, 1)} °C</strong>
          </div>
        </div>
      )}
    </div>
  );
};

// 24h temperature + precipitation chart — full widget version.
// Like TempSparkWidget for the temp line (past from station, future from forecast),
// plus precipitation bars in the same style as the forecast ChartWidget and an
// optional row of forecast icons. Past rain comes from station.measurements
// (5-min increments aggregated into hourly buckets); future rain comes directly
// from hourly[i].precip.
// =========================================================
// Teplota ±24 h — the last 24 h measured by the station (solid line) and the
// next 24 h of the forecast (dashed — a projection), one temperature axis.
// Line coloured by temperature (station page scale); min / max labelled at
// their points; clock ticks every 6 h with day names at midnight; optional
// precipitation in its own lane with its own scale (never on the °C axis).
// Drawn in real pixels (useBox) so text and icons are never stretched.
// =========================================================
window.TempChartWidget = function TempChartWidget({ station, hourly, coords, config = {} }) {
  const showPrecip = config.showPrecip !== false;
  const showStats  = config.showStats  !== false;
  const showMinMax = config.showMinMax !== false;
  const showIcons  = config.showIcons  === true;
  const plotRef = useRef(null);
  const box = window.useBox(plotRef);

  const now = Date.now();
  const past = useMemo(() => (station?.measurements || [])
    .filter(m => m.time && m.temp != null && now - m.time.getTime() <= 24 * 3600e3 && m.time.getTime() <= now)
    .map(m => ({ t: m.time.getTime(), v: m.temp, kind: 'past' })), [station]);
  const future = useMemo(() => (hourly || [])
    .filter(h => h.time.getTime() > (past.length ? past[past.length - 1].t : now - 3600e3))
    .slice(0, 24)
    .map(h => ({ t: h.time.getTime(), v: h.temp, kind: 'future', icon: h.icon, cond: h.cond, mm: h.precip || 0 })), [hourly, past]);
  // Precipitation per hour: measured (summed 5-min increments) and forecast.
  const rain = useMemo(() => {
    const out = [];
    const nowHour = new Date(now); nowHour.setMinutes(0, 0, 0);
    const meas = station?.measurements || [];
    for (let h = -24; h < 0; h++) {
      const a = nowHour.getTime() + h * 3600e3, b = a + 3600e3;
      let sum = 0;
      for (const m of meas) { const t = m.time?.getTime(); if (t >= a && t < b) sum += (m.rain || 0); }
      out.push({ t: a + 1800e3, mm: sum, kind: 'past' });
    }
    future.forEach(f => out.push({ t: f.t, mm: f.mm, kind: 'future' }));
    return out;
  }, [station, future]);

  const all = past.concat(future);
  const lastPast = past.length ? past[past.length - 1] : null;
  // Stats: „teď“ is the station's latest reading (falls back to the forecast).
  const tNow = lastPast ? lastPast.v : (future[0]?.v ?? null);
  const tAgo = past.length ? past[0].v : null;
  const vsYesterday = tNow != null && tAgo != null ? tNow - tAgo : null;
  // trend: change over the last hour of measurements
  const hourAgo = lastPast ? past.find(p => p.t >= lastPast.t - 3600e3) : null;
  const trend = lastPast && hourAgo && lastPast.t - hourAgo.t >= 40 * 60e3 ? lastPast.v - hourAgo.v : null;
  const rng = (a) => (a.length ? [Math.min(...a.map(p => p.v)), Math.max(...a.map(p => p.v))] : null);
  const pastR = rng(past), futR = rng(future);

  let svg = null;
  if (box && box.w > 60 && box.h > 60 && all.length > 1) {
    const W = box.w, H = box.h;
    const dark = window.isDarkTheme();
    const iconBand = showIcons ? 28 : 0;
    const rainH = showPrecip ? Math.max(28, Math.round(H * 0.2)) : 0;
    const fs = window.fontScale();
    const padL = Math.round(38 * fs), padR = 12, padT = Math.round(16 * fs) + iconBand, padB = Math.round(24 * fs);
    const gap = showPrecip ? 8 : 0;
    const tH = H - padT - padB - rainH - gap;                 // temperature area
    const t0 = all[0].t, t1 = all[all.length - 1].t;
    const x = (t) => padL + ((t - t0) / Math.max(1, t1 - t0)) * (W - padL - padR);
    const vals = all.map(p => p.v);
    const vMin = Math.min(...vals), vMax = Math.max(...vals);
    const nt = window.niceTicks(vMin, vMax, Math.max(3, Math.min(7, Math.floor(tH / 24))));
    const y = (v) => padT + tH - ((v - nt.lo) / Math.max(1e-6, nt.hi - nt.lo)) * tH;
    const nowX = x(lastPast ? lastPast.t : now);
    const cc = coords || window.Astronomy.getCoords();
    const nights = [];
    for (let d = new Date(t0 - 86400e3); d.getTime() < t1 + 86400e3; d = new Date(d.getTime() + 86400e3)) {
      const a = window.Astronomy.sunTimes(d, cc.lat, cc.lon).sunset;
      const b = window.Astronomy.sunTimes(new Date(d.getTime() + 86400e3), cc.lat, cc.lon).sunrise;
      if (a && b && b.getTime() > t0 && a.getTime() < t1) nights.push([Math.max(t0, a.getTime()), Math.min(t1, b.getTime())]);
    }

    const pPts = past.map(p => [x(p.t), y(p.v)]);
    const fPts = (lastPast ? [[x(lastPast.t), y(lastPast.v)]] : []).concat(future.map(p => [x(p.t), y(p.v)]));
    const path = (pts) => pts.length > 1 ? window.smoothPath(pts) : '';
    const area = (pts) => pts.length > 1 ? `${path(pts)} L ${pts[pts.length - 1][0]} ${padT + tH} L ${pts[0][0]} ${padT + tH} Z` : '';

    const minI = vals.indexOf(vMin), maxI = vals.indexOf(vMax);
    const mark = (i, kind) => {
      const p = all[i]; const cx = x(p.t), cy = y(p.v);
      const label = `${kind} ${fmtNum(p.v, 1)}°`;
      const tw = label.length * 6.6 * fs + 10;
      const lx = Math.max(padL, Math.min(W - padR - tw, cx - tw / 2));
      // under its point (max too — above the peak it would sit among the
      // „naměřeno | teď | předpověď“ labels); above it only when there's no room
      const ly = cy + 10 + 17 <= padT + tH ? cy + 10 : Math.max(padT, cy - 26);
      return (
        <g key={kind} className="tc-mark">
          <circle cx={cx} cy={cy} r="4.5" fill={window.tempColor(p.v, dark)} stroke="var(--card-strong)" strokeWidth="2" />
          <rect x={lx} y={ly} width={tw} height={17 * fs} rx={8.5 * fs} className="tc-chip" />
          <text x={lx + tw / 2} y={ly + 12 * fs} textAnchor="middle" className="tc-chip-t">{label}</text>
        </g>
      );
    };

    const tickStep = (t1 - t0) / 3600e3 > 30 && W < 520 ? 12 : 6;
    const ticks = window.timeTicks(t0, t1, tickStep);
    const rainMax = Math.max(1, ...rain.map(r => r.mm));
    const rainTop = padT + tH + gap;
    const yR = (mm) => rainTop + rainH - (mm / rainMax) * (rainH - 12);
    const barW = Math.max(2, (3600e3 / Math.max(1, t1 - t0)) * (W - padL - padR) - 2);


    const icons = showIcons ? future.filter((_, i) => i % Math.max(1, Math.ceil(future.length / Math.max(3, Math.floor((W - nowX) / 44)))) === 0) : [];

    svg = (
      <>
        <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} className="tc-svg">
          <defs>
            <window.TempGradient id="tcLine" y={y} lo={nt.lo} hi={nt.hi} dark={dark} />
            <window.TempGradient id="tcFill" y={y} lo={nt.lo} hi={nt.hi} dark={dark} opacity={0.16} />
          </defs>
          {/* nights from the real sunset → sunrise (same shading as the forecast chart) */}
          {nights.map(([a, b]) => (
            <rect key={a} x={x(a)} y={padT} width={Math.max(0, x(b) - x(a))} height={H - padT - padB} className="tc-night" />
          ))}
          {nt.ticks.map(v => (
            <g key={v}>
              <line x1={padL} x2={W - padR} y1={y(v)} y2={y(v)} className="tc-grid" />
              <text x={padL - 6} y={y(v) + 4} textAnchor="end" className="tc-axis">{fmtNum(v, nt.step % 1 ? 1 : 0)}°</text>
            </g>
          ))}
          {ticks.map(d => {
            const tx = x(d.getTime());
            const midnight = d.getHours() === 0;
            return (
              <g key={d.getTime()}>
                <line x1={tx} x2={tx} y1={padT} y2={H - padB + 3} className={midnight ? 'tc-midnight' : 'tc-grid'} />
                <text x={tx} y={H - 7} textAnchor="middle" className={midnight ? 'tc-axis tc-day' : 'tc-axis'}>
                  {midnight ? `${WeatherData.WD_SHORT[d.getDay()]} ${d.getDate()}.` : `${d.getHours()}:00`}
                </text>
              </g>
            );
          })}
          <path d={area(pPts)} fill="url(#tcFill)" />
          <path d={area(fPts)} fill="url(#tcFill)" opacity="0.55" />
          <path d={path(pPts)} fill="none" stroke="url(#tcLine)" strokeWidth="2.2" strokeLinejoin="round" />
          <path d={path(fPts)} fill="none" stroke="url(#tcLine)" strokeWidth="2.2" strokeDasharray="6 5" strokeLinecap="round" />
          {showPrecip && (
            <g>
              <line x1={padL} x2={W - padR} y1={rainTop + rainH} y2={rainTop + rainH} className="tc-grid" />
              {rain.map((r, i) => r.mm > 0.05 && (
                <rect key={i} x={x(r.t) - barW / 2} y={yR(r.mm)} width={barW} height={rainTop + rainH - yR(r.mm)}
                      rx="2" className={`tc-rain ${r.kind}`} />
              ))}
              {!rain.some(r => r.mm > 0.05) && (
                <text x={W - padR} y={rainTop + rainH - 6} textAnchor="end" className="tc-axis">srážky: žádné</text>
              )}
              {rain.some(r => r.mm > 0.05) && (
                <text x={W - padR} y={rainTop + 10} textAnchor="end" className="tc-axis">srážky · max {fmtNum(rainMax, 1)} mm/h</text>
              )}
            </g>
          )}
          <line x1={nowX} x2={nowX} y1={padT - 4} y2={H - padB} className="tc-now" />
          <text x={nowX} y={padT - 7} textAnchor="middle" className="tc-now-t">teď</text>
          {nowX - padL > 150 && <text x={nowX - 26} y={padT - 7} textAnchor="end" className="tc-side">← naměřeno</text>}
          {W - padR - nowX > 150 && <text x={nowX + 26} y={padT - 7} className="tc-side">předpověď →</text>}
          {showMinMax && maxI >= 0 && mark(maxI, 'max')}
          {showMinMax && minI >= 0 && minI !== maxI && mark(minI, 'min')}
          {icons.map((f, i) => (
            <image key={i} href={WeatherData.iconUrl(f.icon)} x={x(f.t) - 11} y={4} width="22" height="22">
              <title>{f.cond}</title>
            </image>
          ))}
        </svg>
        <window.ChartHover W={W} H={H} top={padT} bottom={H - padB}
          xs={all.map(p => x(p.t))}
          marks={(i) => [{ y: y(all[i].v), color: window.tempColor(all[i].v, dark) }]}
          tip={(i) => {
            const hp = all[i];
            const hRain = rain.find(r => Math.abs(r.t - hp.t) <= 1800e3);
            return (<>
              <div className="tip-time">{window.fmtTipTime(hp.t)}
                <small>{hp.kind === 'past' ? 'naměřeno' : 'předpověď'}</small></div>
              <div className="tip-row"><span className="k"><i className="tip-sw" style={{ background: window.tempColor(hp.v, dark) }} />Teplota</span><strong>{fmtNum(hp.v, 1)} °C</strong></div>
              {showPrecip && hRain && <div className="tip-row"><span className="k"><i className="tip-sw" style={{ background: '#3b82f6' }} />Srážky</span><strong>{hRain.mm > 0.05 ? `${fmtNum(hRain.mm, 1)} mm/h` : 'žádné'}</strong></div>}
              {lastPast && hp.kind === 'past' && hp.t !== lastPast.t && (
                <div className="tip-foot">proti teď {hp.v - lastPast.v > 0.05 ? '+' : hp.v - lastPast.v < -0.05 ? '−' : '±'}{window.czNum(Math.abs(hp.v - lastPast.v), 1)} °C</div>
              )}
            </>);
          }} />
      </>
    );
  }

  if (!all.length) return <div className="muted">Načítání…</div>;
  return (
    <div className="tc">
      <div className="tc-plot" ref={plotRef}>{svg}</div>
      {showStats && (
        <div className="tc-stats">
          <div className="is-now"><small>teď</small>
            <b>{fmtNum(tNow, 1)}°{trend != null && Math.abs(trend) >= 0.2 && (
              <span className={`tc-trend ${trend > 0 ? 'up' : 'down'}`} title="Změna za poslední hodinu">
                <i className={`bi bi-arrow-${trend > 0 ? 'up' : 'down'}-right`} aria-hidden="true"></i>{window.czNum(Math.abs(trend), 1)}°/h
              </span>
            )}</b>
          </div>
          <div><small>naměřeno 24 h</small><b className="tc-range">{pastR ? <>{fmtNum(pastR[0], 1)}<span>až</span>{fmtNum(pastR[1], 1)}°</> : '—'}</b></div>
          <div><small>předpověď 24 h</small><b className="tc-range">{futR ? <>{fmtNum(futR[0], 0)}<span>až</span>{fmtNum(futR[1], 0)}°</> : '—'}</b></div>
          <div><small>proti včerejšku</small>
            <b className={vsYesterday > 0.3 ? 'up' : vsYesterday < -0.3 ? 'down' : ''}>
              {vsYesterday == null ? '—' : (vsYesterday > 0 ? '+' : vsYesterday < 0 ? '−' : '±') + window.czNum(Math.abs(vsYesterday), 1)}°
            </b>
          </div>
        </div>
      )}
    </div>
  );
};


// Single big-detail widget — pick one sensor and show big number + trend + dayMinMax
window.HighlightWidget = function HighlightWidget({ station, hourly, metric = 'temp' }) {
  const s = station?.latest;
  const h0 = (hourly || [])[0];
  const M = {
    temp:    { label: 'Teplota', unit: '°C', sf: 'TeplotaVnejsi', hf: 'temp',     icon: 'thermometer-half', dec: 1 },
    humidity:{ label: 'Vlhkost', unit: '%',  sf: 'VlhkostVnejsi', hf: 'humidity', icon: 'droplet-half', dec: 0 },
    pressure:{ label: 'Tlak',    unit: 'hPa',sf: 'TlakRel',       hf: 'pressure', icon: 'arrows-collapse', dec: 0 },
    wind:    { label: 'Vítr',    unit: (window.Units ? window.Units.windLabel() : 'm/s'), sf: 'Vitr', hf: 'wind', icon: 'wind', dec: 1, convert: 'wind' },
    uv:      { label: 'UV index',unit: '',   sf: 'UVindex',       hf: 'uv',       icon: 'sun',           dec: 1 },
    rain:    { label: 'Srážky dnes', unit: 'mm', sf: 'SrazkyDen', hf: 'precip',   icon: 'cloud-rain',    dec: 1 },
  };
  const m = M[metric] || M.temp;
  const conv = m.convert === 'wind' && window.Units
    ? (x) => window.Units.wind(x)
    : (x) => x;
  const v = conv(s?.[m.sf] ?? h0?.[m.hf]);
  const dayMM = station?.dayMinMax?.[m.sf];
  const dayMin = dayMM ? conv(dayMM.min) : null;
  const dayMax = dayMM ? conv(dayMM.max) : null;
  const trend = (s && station.prev && station.prev[m.sf] != null)
    ? conv(s[m.sf]) - conv(station.prev[m.sf]) : null;

  return (
    <div className="highlight-widget">
      <div className="hl-head">
        <i className={`bi bi-${m.icon}`}></i>
        <span>{m.label}</span>
      </div>
      <div className="hl-value">
        {fmtNum(v, m.dec)}<small>{m.unit}</small>
      </div>
      <div className="hl-meta">
        {dayMM && (
          <span className="hl-mm">
            <i className="bi bi-caret-down-fill"></i>{fmtNum(dayMin, m.dec)} ·
            <i className="bi bi-caret-up-fill"></i>{fmtNum(dayMax, m.dec)} {m.unit}
          </span>
        )}
        {trend != null && (
          <span className={`hl-trend ${trend > 0.05 ? 'up' : trend < -0.05 ? 'down' : 'flat'}`}>
            {trend > 0 ? '↑' : trend < 0 ? '↓' : '→'} {fmtNum(Math.abs(trend), m.dec)}
          </span>
        )}
      </div>
    </div>
  );
};
