// app.jsx — root: header, gauge strip, chart stack, alert log, settings, tweaks.

const { useState, useEffect, useRef, useMemo } = React;

// Time format for the small line under the chart-header value: HH:MM for
// today, MM/DD HH:MM for older points so it's still unambiguous in 7D / 30D
// views without taking up much horizontal space.
function formatHoverTime(t) {
  const d = new Date(t);
  const now = new Date();
  if (d.toDateString() === now.toDateString()) {
    return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
  }
  return d.toLocaleString([], {
    month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', hour12: false,
  });
}

const DEFAULT_THRESHOLDS = {
  temp:     { warnLow: 16,  warnHigh: 28,   dangerLow: 12,  dangerHigh: 32 },
  humidity: { warnLow: 30,  warnHigh: 65,   dangerLow: 20,  dangerHigh: 80 },
  co2:      { warnLow: null, warnHigh: 1000, dangerLow: null, dangerHigh: 2000 },
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "gaugeStyle": "bar",
  "visibleSensors": ["co2", "temp", "humidity"],
  "thresholdsCo2_warnHigh": 1000,
  "thresholdsCo2_dangerHigh": 2000,
  "thresholdsTemp_warnLow": 16,
  "thresholdsTemp_warnHigh": 28,
  "thresholdsTemp_dangerLow": 12,
  "thresholdsTemp_dangerHigh": 32,
  "thresholdsHumidity_warnLow": 30,
  "thresholdsHumidity_warnHigh": 65,
  "thresholdsHumidity_dangerLow": 20,
  "thresholdsHumidity_dangerHigh": 80
}/*EDITMODE-END*/;

// ---- Connection status pill ---------------------------------------------
// Server-authoritative: the pill reflects when the Arduino last successfully
// POSTed to the server (recorded with the SERVER's clock, persisted in the
// device_status table, polled via /api/status every 20s). It does NOT depend
// on the browser's data-fetch cadence, the page reload time, or the Arduino's
// own clock — open the dashboard at any moment and the indicator is correct.
function StatusPill({ store }) {
  const [, setTick] = useState(0);
  useEffect(() => {
    const unsub = store.subscribe(() => setTick(t => t + 1));
    setTick(t => t + 1);
    // Tick every second so the "X AGO" counter advances and the pill flips
    // to OFFLINE the moment ago crosses the 120s threshold (the next status
    // poll might be up to 20s away, but the local clock keeps the UI honest).
    const id = setInterval(() => setTick(t => t + 1), 1000);
    return () => { unsub(); clearInterval(id); };
  }, [store]);
  const ago = store.lastIngestAt != null
    ? Math.max(0, Math.floor((Date.now() - store.lastIngestAt) / 1000))
    : null;
  const live = ago != null && ago < 120;
  const fmtAgo = a => {
    if (a == null) return '—';
    if (a < 60) return `${a}S`;
    if (a < 3600) return `${Math.floor(a/60)}M`;
    if (a < 86400) return `${Math.floor(a/3600)}H`;
    return `${Math.floor(a/86400)}D`;
  };
  return (
    <div className={`status-pill-hd ${live ? 'live' : 'stale'}`}>
      <span className="status-led"/>
      <span className="status-txt mono">{live ? 'LIVE' : 'OFFLINE'}</span>
      <span className="status-sep"/>
      <span className="status-meta mono">UPDATED {fmtAgo(ago)} AGO</span>
    </div>
  );
}

// ---- Alert log ----------------------------------------------------------
const ALERT_STORAGE_KEY = 'datasite.alertLog.v1';
const ALERT_RETENTION_MS = 24 * 3600 * 1000;
const ALERT_MAX = 200;

function loadStoredAlerts() {
  try {
    const raw = localStorage.getItem(ALERT_STORAGE_KEY);
    if (!raw) return [];
    const cutoff = Date.now() - ALERT_RETENTION_MS;
    return JSON.parse(raw)
      .filter(e => e.t > cutoff)
      .map(e => ({
        id: e.id, t: e.t, status: e.status, from: e.from, value: e.value,
        sensor: SENSOR_DEFS.find(s => s.id === e.sensorId)
             || { id: e.sensorId, label: e.sensorId, unit: '', decimals: 0 },
      }));
  } catch (_) { return []; }
}

function saveStoredAlerts(arr) {
  try {
    const stripped = arr.slice(0, ALERT_MAX).map(e => ({
      id: e.id, t: e.t, sensorId: e.sensor.id, status: e.status, from: e.from, value: e.value,
    }));
    localStorage.setItem(ALERT_STORAGE_KEY, JSON.stringify(stripped));
  } catch (_) {}
}

function classify(v, t) {
  if (v == null) return 'ok';
  if ((t.dangerLow != null && v <= t.dangerLow) || (t.dangerHigh != null && v >= t.dangerHigh)) return 'danger';
  if ((t.warnLow  != null && v <= t.warnLow)  || (t.warnHigh  != null && v >= t.warnHigh))  return 'warn';
  return 'ok';
}

function useAlertLog(store, sensors, thresholds) {
  const [log, setLog] = useState(loadStoredAlerts);
  const stateRef = useRef({});

  // Seed stateRef from the loaded log so we don't re-emit alerts for a state
  // we were already in before the reload.
  useEffect(() => {
    const seen = new Set();
    for (const e of log) {
      const sid = e.sensor.id;
      if (seen.has(sid)) continue;
      seen.add(sid);
      stateRef.current[sid] = e.status === 'clear' ? 'ok' : e.status;
    }
    // eslint-disable-next-line
  }, []);

  // Persist on every change (with sliding 24h window).
  useEffect(() => {
    saveStoredAlerts(log);
  }, [log]);

  // Live: subscribe and detect transitions on each store update.
  useEffect(() => {
    return store.subscribe(() => {
      const next = [];
      sensors.forEach(s => {
        const cur = store.current(s.id);
        if (!cur) return;
        const status = classify(cur.v, thresholds[s.id] || {});
        const prev = stateRef.current[s.id];
        if (prev && prev !== 'ok' && status === 'ok') {
          next.push({ id: `${s.id}-${cur.t}-clr`, t: cur.t, sensor: s, status: 'clear', from: prev, value: cur.v });
        }
        if (status !== 'ok' && prev !== status) {
          next.push({ id: `${s.id}-${cur.t}-${status}`, t: cur.t, sensor: s, status, value: cur.v });
        }
        stateRef.current[s.id] = status;
      });
      if (next.length) {
        setLog(prev => mergeAlerts(prev, next));
      }
    });
  }, [store, sensors, thresholds]);

  // Re-derive from history once boot finishes (catches transitions that
  // happened while the page was closed). Idempotent thanks to id dedup.
  useEffect(() => {
    if (!store.ready) return;
    const derived = [];
    sensors.forEach(s => {
      const t = thresholds[s.id] || {};
      const hist = store.series(s.id, ALERT_RETENTION_MS);
      let prev = 'ok';
      for (const p of hist) {
        const status = classify(p.v, t);
        if (status !== 'ok' && prev !== status) {
          derived.push({ id: `${s.id}-${p.t}-${status}`, t: p.t, sensor: s, status, value: p.v });
        } else if (prev !== 'ok' && status === 'ok') {
          derived.push({ id: `${s.id}-${p.t}-clr`, t: p.t, sensor: s, status: 'clear', from: prev, value: p.v });
        }
        prev = status;
      }
    });
    if (derived.length) setLog(prev => mergeAlerts(prev, derived));
  }, [store.ready, sensors, thresholds]);

  return log;
}

function mergeAlerts(prev, additions) {
  const seen = new Set(prev.map(e => e.id));
  const fresh = additions.filter(e => !seen.has(e.id));
  if (!fresh.length) {
    // Still drop expired entries on every merge attempt so the log stays bounded.
    const cutoff = Date.now() - ALERT_RETENTION_MS;
    const trimmed = prev.filter(e => e.t > cutoff);
    return trimmed.length === prev.length ? prev : trimmed;
  }
  const cutoff = Date.now() - ALERT_RETENTION_MS;
  return [...fresh, ...prev]
    .filter(e => e.t > cutoff)
    .sort((a, b) => b.t - a.t)
    .slice(0, ALERT_MAX);
}

function AlertLog({ entries }) {
  const fmtTime = t => {
    const d = new Date(t);
    const ago = Date.now() - t;
    if (ago < 60_000) return `${Math.floor(ago/1000)}S AGO`;
    if (ago < 3600_000) return `${Math.floor(ago/60000)}M AGO`;
    if (ago < 86400_000) return `${Math.floor(ago/3600000)}H AGO`;
    return d.toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false });
  };
  return (
    <div className="alert-log">
      <div className="alert-log-hd">
        <span className="mono-lbl">EVENT LOG</span>
        <span className="mono muted small">{entries.length} ENTRIES</span>
      </div>
      {entries.length === 0 && (
        <div className="alert-empty mono">— NO EVENTS RECORDED —</div>
      )}
      {entries.map(e => (
        <div key={e.id} className={`alert-row ${e.status}`}>
          <span className="alert-time mono">{fmtTime(e.t)}</span>
          <span className="alert-bar" data-status={e.status}/>
          <span className="alert-sensor mono">{e.sensor.label.toUpperCase()}</span>
          <span className="alert-msg">
            {e.status === 'clear'
              ? <span><b>CLEARED</b> · returned to nominal from {e.from.toUpperCase()}</span>
              : <span><b>{e.status === 'danger' ? 'DANGER' : 'WARN'}</b> · threshold crossed</span>}
          </span>
          <span className="alert-val mono">{Number(e.value).toFixed(e.sensor.decimals)} {e.sensor.unit}</span>
        </div>
      ))}
    </div>
  );
}

// ---- Main app -----------------------------------------------------------
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const storeRef = useRef(null);
  if (!storeRef.current) storeRef.current = new SensorStore();
  const store = storeRef.current;

  const [, force] = useState(0);
  useEffect(() => {
    const unsub = store.subscribe(() => force(x => x + 1));
    // Boot may have resolved between render and effect mount. Pick that up
    // immediately so the dashboard never sits empty waiting for the next tick.
    force(x => x + 1);
    return unsub;
  }, [store]);

  // Thresholds: read from tweak keys; settings panel writes back via setTweak.
  const thresholds = useMemo(() => {
    const cap = s => s.charAt(0).toUpperCase() + s.slice(1);
    const out = {};
    SENSOR_DEFS.forEach(s => {
      const k = cap(s.id);
      out[s.id] = {
        warnLow:    t[`thresholds${k}_warnLow`] ?? null,
        warnHigh:   t[`thresholds${k}_warnHigh`] ?? null,
        dangerLow:  t[`thresholds${k}_dangerLow`] ?? null,
        dangerHigh: t[`thresholds${k}_dangerHigh`] ?? null,
      };
    });
    return out;
  }, [t]);

  const setThresholds = (updater) => {
    const next = typeof updater === 'function' ? updater(thresholds) : updater;
    const cap = s => s.charAt(0).toUpperCase() + s.slice(1);
    const edits = {};
    Object.keys(next).forEach(sid => {
      const k = cap(sid);
      ['warnLow','warnHigh','dangerLow','dangerHigh'].forEach(p => {
        edits[`thresholds${k}_${p}`] = next[sid]?.[p] ?? null;
      });
    });
    setTweak(edits);
  };

  const visibleSensors = t.visibleSensors || ['temp','humidity','co2'];
  const setVisibleSensors = (updater) => {
    const next = typeof updater === 'function' ? updater(visibleSensors) : updater;
    setTweak('visibleSensors', next);
  };

  const sensors = SENSOR_DEFS.filter(s => visibleSensors.includes(s.id));

  const [timeframe, setTimeframe] = useState('24h');
  const [settingsOpen, setSettingsOpen] = useState(false);
  // SHARED hover timestamp — set by whichever chart's cursor the mouse is
  // currently over. All charts read this same value and resolve it against
  // their own data, so the cursors stay synchronized: hover CO₂ at 14:32
  // and Temperature + Humidity show their 14:32 readings too.
  const [hoverT, setHoverT] = useState(null);

  const alerts = useAlertLog(store, SENSOR_DEFS, thresholds);

  return (
    <div className="page">
      <header className="topbar">
        <div className="brand">
          <div className="brand-text">
            <div className="brand-name">ATMOSPHERE</div>
            <div className="brand-sub mono">SENSOR · TELEMETRY · ARDUINO/ESP32</div>
          </div>
        </div>
        <div className="topbar-center">
          <StatusPill store={store}/>
        </div>
        <div className="topbar-right">
          <button className="ghost-btn" onClick={() => setSettingsOpen(true)}>
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
              <circle cx="7" cy="7" r="2.25" stroke="currentColor" strokeWidth="1.5"/>
              <path d="M7 1V2.5M7 11.5V13M13 7H11.5M2.5 7H1M11.24 2.76L10.18 3.82M3.82 10.18L2.76 11.24M11.24 11.24L10.18 10.18M3.82 3.82L2.76 2.76" stroke="currentColor" strokeWidth="1.5" strokeLinecap="square"/>
            </svg>
            <span>CONFIG</span>
          </button>
        </div>
      </header>

      <section className="gauge-strip" data-count={sensors.length}>
        {sensors.map(s => {
          const cur = store.current(s.id);
          const ths = thresholds[s.id] || {};
          return (
            <div key={s.id} className="gauge-cell">
              <div className="cell-hd">
                <span className="cell-idx mono">{String(SENSOR_DEFS.indexOf(s) + 1).padStart(2, '0')}</span>
                <span className="cell-label">{s.label.toUpperCase()}</span>
                <span className={`cell-status mono`} data-status={statusFor(cur?.v, ths)}>
                  {statusFor(cur?.v, ths) === 'ok' ? 'OK' : statusFor(cur?.v, ths).toUpperCase()}
                </span>
              </div>
              <Gauge
                style={t.gaugeStyle}
                value={cur?.v}
                min={s.range[0]}
                max={s.range[1]}
                unit={s.unit}
                label={s.label}
                decimals={s.decimals}
                thresholds={ths}
              />
            </div>
          );
        })}
      </section>

      <section className="chart-section">
        <div className="chart-controls">
          <div className="control-left">
            <span className="mono-lbl">TIMEFRAME</span>
            <div className="tf-pills">
              {TIMEFRAMES.map(tf => (
                <button key={tf.id} className={`tf-pill ${timeframe === tf.id ? 'active' : ''}`}
                  onClick={() => setTimeframe(tf.id)}>{tf.label}</button>
              ))}
            </div>
          </div>
        </div>

        <div className="chart-stack">
          {sensors.map(s => {
            const cur = store.current(s.id);
            const ths = thresholds[s.id] || {};
            // Resolve the shared hoverT against THIS sensor's data so each
            // chart's header shows its own value at the cursor's timestamp.
            const tfDef = TIMEFRAMES.find(tt => tt.id === timeframe) || TIMEFRAMES.find(tt => tt.id === '24h');
            const sensorData = hoverT != null ? store.series(s.id, tfDef.ms) : null;
            const hovered = sensorData ? findNearestPoint(sensorData, hoverT) : null;
            const shown = hovered || cur;
            return (
              <div key={s.id} className="chart-block">
                <div className="chart-hd">
                  <div className="chart-hd-l">
                    <span className="chart-idx mono">{String(SENSOR_DEFS.indexOf(s) + 1).padStart(2, '0')}</span>
                    <h3 className="chart-title">{s.label}</h3>
                    <span className="chart-unit mono">[ {s.unit} ]</span>
                  </div>
                  <div className="chart-hd-r">
                    <span className="chart-current mono" style={{ color: STATUS_COLOR[statusFor(shown?.v, ths)] }}>
                      {fmt(shown?.v, s.decimals)}<span className="chart-current-unit"> {s.unit}</span>
                    </span>
                    {shown && (
                      <span className="chart-current-time mono">{formatHoverTime(shown.t)}</span>
                    )}
                  </div>
                </div>
                <SensorChart
                  sensor={s}
                  store={store}
                  timeframe={timeframe}
                  thresholds={ths}
                  decimals={s.decimals}
                  hoverT={hoverT}
                  onHoverT={setHoverT}
                />
              </div>
            );
          })}
          {sensors.length === 0 && (
            <div className="chart-block empty mono">
              ALL CHANNELS HIDDEN — RE-ENABLE FROM CONFIG.
            </div>
          )}
        </div>
      </section>

      <section className="alerts-section">
        <AlertLog entries={alerts}/>
      </section>

      <footer className="page-ft mono">
        <span>ATMOSPHERE v0.1</span>
        <span className="muted">·</span>
        <span>{SENSOR_DEFS.length} CHANNELS REGISTERED</span>
        <span className="muted">·</span>
        <span>STEP 1MIN · LIVE 2S</span>
        <span className="muted">·</span>
        <span>{new Date().toISOString().slice(0,10)}</span>
      </footer>

      <SettingsPanel
        open={settingsOpen}
        onClose={() => setSettingsOpen(false)}
        sensors={SENSOR_DEFS}
        thresholds={thresholds}
        setThresholds={setThresholds}
        visibleSensors={visibleSensors}
        setVisibleSensors={setVisibleSensors}
      />

      <TweaksPanel>
        <TweakSection label="Display"/>
        <TweakSelect label="Gauge style" value={t.gaugeStyle}
          options={[
            { value: 'bar',    label: 'Linear bar' },
            { value: 'ring',   label: 'Radial ring' },
            { value: 'dial',   label: 'Semi-dial' },
            { value: 'number', label: 'Big number' },
            { value: 'scale',  label: 'Tick scale' },
          ]}
          onChange={(v) => setTweak('gaugeStyle', v)}/>

        <TweakSection label="Channels"/>
        {SENSOR_DEFS.map(s => (
          <TweakToggle key={s.id} label={s.label}
            value={visibleSensors.includes(s.id)}
            onChange={(v) => setVisibleSensors(v
              ? [...new Set([...visibleSensors, s.id])]
              : visibleSensors.filter(x => x !== s.id))}/>
        ))}

        <TweakSection label="Thresholds"/>
        <TweakButton label="OPEN CONFIG →" onClick={() => setSettingsOpen(true)}/>
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
