// HomeHub shared state — real API + light normalization
// A small in-memory store with React hook

// ── API helper ───────────────────────────────────────────────────────────────
async function api(path, opts = {}) {
  const res = await fetch(`/api${path}`, {
    credentials: 'include',
    headers: { 'Content-Type': 'application/json', ...opts.headers },
    ...opts,
  });
  if (!res.ok) {
    let detail = '';
    try {
      const raw = await res.text();
      if (raw) {
        try {
          const parsed = JSON.parse(raw);
          detail = parsed?.error || parsed?.message || raw;
        } catch {
          detail = raw;
        }
      }
    } catch {}
    if (res.status === 401) window.dispatchEvent(new Event('auth:logout'));
    throw new Error(detail ? `API ${path} → ${res.status}: ${detail}` : `API ${path} → ${res.status}`);
  }
  return res.json();
}

// ── Normalisers ──────────────────────────────────────────────────────────────

const WASTE_ICONS = { trash:'🗑', general:'🗑', paper:'📄', plastic:'🧴', organic:'🌱', glass:'🍾', metal:'🥫', electronics:'📱', textile:'👕', pmd:'♻️' };
const SCENE_EMOJI = { sparkles:'✨', sun:'☀', moon:'🌙', film:'🎬', movie:'🎬', suitcase:'🧳', candle:'🕯', book:'📚', coffee:'☕', home:'🏠', bolt:'⚡', wave:'🌊', leaf:'🌿', party:'🎉', music:'🎵', star:'⭐' };

function normTodo(t) {
  return { id: t.id, text: t.title || t.text || '', done: !!t.done, assign: t.assigned_to || '', list: t.category || 'general', due: fmtDue(t.due_date) };
}

function normShop(s) {
  return { id: s.id, text: s.item || s.text || '', done: !!s.checked, qty: s.quantity || '', cat: s.category || 'general', added: s.added_by || '' };
}

function normNote(n) {
  const lines = (n.content || '').split('\n');
  return { id: n.id, title: lines[0] || '—', body: lines.slice(1).join('\n'), color: n.color || '#F0DCA8', pinned: !!n.pinned };
}

function resolveUiDeviceType(device) {
  const rawType = String(device?.type || '').toLowerCase();
  const integration = String(device?.integration || '').toLowerCase();
  const name = String(device?.name || '').toLowerCase();
  const icon = String(device?.icon || '').toLowerCase();

  if (rawType === 'camera' || integration === 'camera_local') return 'camera';
  if (integration === 'tuya_cloud' && (/(^|[^a-z])(camera|cam|doorbell|video)([^a-z]|$)/i.test(name) || icon.includes('camera'))) {
    return 'camera';
  }

  const typeMap = { light: 'bulb', strip: 'bulb', plug: 'plug', ac: 'ac', washer: 'washer', camera: 'camera' };
  return typeMap[rawType] || rawType;
}

function normDevice(d) {
  const runtime = deriveDeviceMetrics(d.state || {}, { on: false, value: 0, watts: 0 });
  return {
    id: d.id, name: d.name, room: d.room || 'Unknown',
    type: resolveUiDeviceType(d),
    brand: d.integration || '',
    on: runtime.on ?? false,
    value: runtime.value ?? 0,
    watts: runtime.watts ?? 0,
    power: runtime.power ?? 0,
    temp: runtime.temp,
    target: runtime.target,
    mode: runtime.mode,
    status: runtime.status || '',
    progress: runtime.progress || 0,
    _raw: d,
  };
}

function shouldPollDeviceState(device) {
  if (!device) return false;
  if (device.type === 'camera') return false;
  const integration = String(device._raw?.integration || device.brand || '').toLowerCase();
  return integration === 'smartthings' || integration === 'tuya' || integration === 'tuya_cloud' || integration === 'local_http';
}

function normScene(s) {
  const icon = (s.icon || '').toLowerCase();
  const emoji = SCENE_EMOJI[icon] || '✨';
  const actions = Array.isArray(s.actions) ? s.actions : [];
  return {
    id: s.id, name: s.name, emoji, color: s.color || 'var(--hh-accent)',
    active: false,
    devices: actions.length,
    note: actions.map(a => a.label || a.type || '').filter(Boolean).join(', ') || '',
    _raw: s,
  };
}

function normRule(r) {
  const conditions = Array.isArray(r.conditions) ? r.conditions : [];
  const actions = Array.isArray(r.actions) ? r.actions : [];
  const when = conditions.map(c => c.label || c.type || '').filter(Boolean).join(' and ') || '…';
  const then = actions.map(a => a.label || a.type || '').filter(Boolean);
  return {
    id: r.id, name: r.name,
    active: !!r.enabled,
    text: r.description || r.name,
    when, then: then.length ? then : ['(action)'],
    runs: r.trigger_count || r.run_count || 0,
    lastRun: r.last_triggered ? fmtAgo(r.last_triggered) : (r.last_run_at ? fmtAgo(r.last_run_at) : 'Never'),
    _raw: r,
  };
}

function deriveDeviceMetrics(state = {}, current = {}) {
  const powerOn = state['20'] === true || state['1'] === true || state.switch === true || state.power === true;
  const powerOff = state['20'] === false || state['1'] === false || state.switch === false || state.power === false;
  const brightnessValue = state['22'] || state.bright_value || state.brightness;
  const watts = state.power_consumption?.power || state.power || current.watts || 0;
  const temp = state.temperature ?? state.current_temperature ?? state.temp_current ?? current.temp;
  const target = state.setpoint ?? state.cooling_setpoint ?? state.heating_setpoint ?? current.target;
  const mode = state.mode || state.work_mode || current.mode;
  return {
    on: powerOn ? true : (powerOff ? false : current.on),
    value: brightnessValue != null ? Math.max(0, Math.min(100, Math.round(Number(brightnessValue) / (Number(brightnessValue) > 100 ? 10 : 1)))) : (current.value || 0),
    watts: watts || 0,
    power: watts || 0,
    temp,
    target,
    mode,
    status: state.machineState || state.machine_state || current.status || '',
    progress: state.progress || current.progress || 0,
  };
}

function mergeDeviceRuntime(device, state) {
  if (!state || typeof state !== 'object') return device;
  return {
    ...device,
    ...deriveDeviceMetrics(state, device),
    _state: state,
  };
}

function normWaste(w) {
  const iconKey = (w.icon || '').toLowerCase();
  const icon = WASTE_ICONS[iconKey] || '🗑';
  return {
    id: w.id, type: w.type, color: w.color || '#2A211A', icon,
    day: w.next_date ? new Date(w.next_date).toLocaleDateString('en', { weekday: 'short' }) : '?',
    next: w.next_date ? fmtRelDate(w.next_date) : '?',
    _raw: w,
  };
}

function normCalEvent(e) {
  // Compute day offset from today (0=today, 1=tomorrow...)
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const d = new Date(e.date + 'T00:00:00'); d.setHours(0, 0, 0, 0);
  const diff = Math.round((d - today) / 86400000);
  const day = diff >= 0 && diff < 7 ? diff : -1;
  const dur = e.time && e.end_time ? timeDiff(e.time, e.end_time) : '';
  return {
    id: e.id, title: e.title, time: e.time || '', end_time: e.end_time || '', duration: dur,
    color: e.color || 'var(--hh-accent)', day,
    category: e.category || e.description || '',
    date: e.date,
    source: e.source || 'local', // local | google | waste
  };
}

function normGeofence(g) {
  return {
    id: String(g.id),
    name: g.name || 'Zone',
    radius: Number(g.radius) || 120,
    members: Array.isArray(g.members) ? g.members.map(String) : [],
    color: g.color || '#3b82f6',
    lat: Number(g.lat) || 52.370,
    lng: Number(g.lon ?? g.lng) || 4.895,
    address: g.address || '',
    notify: g.notify !== 0 && g.notify !== false,
    icon: g.icon || 'pin',
  };
}

function toApiGeofence(z) {
  return {
    id: z.id,
    name: z.name,
    lat: z.lat,
    lon: z.lng,
    radius: z.radius,
    icon: z.icon || 'pin',
    color: z.color || '#3b82f6',
    notify: z.notify !== false,
  };
}

function normActivity(n) {
  const catMap = { todo: 'list', shopping: 'list', calendar: 'list', device: 'device', rule: 'rule', scene: 'scene', waste: 'list', geo: 'geo', geofence: 'geo', battery_low: 'geo', automation: 'rule', camera: 'camera' };
  const cat = catMap[n.type] || 'device';
  return {
    id: n.id,
    time: new Date(n.created_at).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' }),
    who: n.user_for || 'System',
    action: n.message || n.title || '',
    cat,
    created_at: n.created_at,
    timestamp: n.timestamp || null,
    title: n.title || '',
    description: n.message || n.title || '',
    raw: n,
  };
}

function normUser(u) {
  const COLORS = ['#C9562E','#6E8662','#C99A2E','#7A4F6B','#5F7E91'];
  const idx = (u.id || 0) % COLORS.length;
  return {
    id: String(u.id), name: u.name,
    role: u.role || 'Member',
    color: COLORS[idx],
    initials: u.name ? u.name.slice(0, 2).toUpperCase() : '?',
    location: '',
    battery: null, tracked: false,
    lat: null, lng: null,   // null until tracker data arrives — prevents ghost pins
    avatar: u.avatar || '👤',
    photo_url: u.photo_url || null,
    tracker_id: u.tracker_id || '',
    timestamp: 0,
  };
}

// ── Date helpers ─────────────────────────────────────────────────────────────

function fmtDue(dateStr) {
  if (!dateStr) return '';
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const d = new Date(dateStr + 'T00:00:00'); d.setHours(0, 0, 0, 0);
  const diff = Math.round((d - today) / 86400000);
  if (diff === 0) return 'Today';
  if (diff === 1) return 'Tomorrow';
  if (diff === -1) return 'Yesterday';
  if (diff > 1 && diff < 7) return d.toLocaleDateString('en', { weekday: 'short' });
  if (diff < 0) return 'Overdue';
  return d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
}

function fmtRelDate(dateStr) {
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const d = new Date(dateStr + 'T00:00:00'); d.setHours(0, 0, 0, 0);
  const diff = Math.round((d - today) / 86400000);
  if (diff === 0) return 'Today';
  if (diff === 1) return 'Tomorrow';
  if (diff < 0) return 'Past';
  return `In ${diff} days`;
}

function fmtAgo(isoStr) {
  const diff = (Date.now() - new Date(isoStr)) / 1000;
  if (diff < 60) return 'Just now';
  if (diff < 3600) return `${Math.round(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.round(diff / 3600)}h ago`;
  return `${Math.round(diff / 86400)}d ago`;
}

function timeDiff(t1, t2) {
  if (!t1 || !t2) return '';
  const [h1, m1] = t1.split(':').map(Number);
  const [h2, m2] = t2.split(':').map(Number);
  const mins = (h2 * 60 + m2) - (h1 * 60 + m1);
  if (mins <= 0) return '';
  if (mins < 60) return `${mins}m`;
  return `${Math.floor(mins / 60)}h${mins % 60 ? (mins % 60) + 'm' : ''}`;
}

// ── Fallback mock data (used when real data is unavailable) ──────────────────

const FALLBACK_FAMILY = [
  { id: 'f1', name: 'Raph', role: 'Admin', color: '#C9562E', initials: 'RA', location: 'Home', battery: 74, tracked: true, lat: 52.370, lng: 4.895 },
  { id: 'f2', name: 'Mira', role: 'Member', color: '#6E8662', initials: 'MI', location: 'Work', battery: 52, tracked: true, lat: 52.358, lng: 4.870 },
  { id: 'f3', name: 'Yuki', role: 'Kid', color: '#C99A2E', initials: 'YU', location: 'School', battery: 88, tracked: true, lat: 52.372, lng: 4.905 },
];
const FALLBACK_GEOFENCES = [
  { id: 'g1', name: 'Home', radius: 120, members: ['f1'], color: '#C9562E', lat: 52.370, lng: 4.895, address: '' },
];
const FALLBACK_CAMERAS = [
  { id: 'c1', name: 'Front door', location: 'Entry', status: 'live', motion: '—', hue: 'hsl(20, 40%, 50%)' },
];

// ─────────────────────────────────────────────────────────────
// Store — real API
function useStore() {
  const [devices, setDevices] = React.useState([]);
  const [scenes, setScenes] = React.useState([]);
  const [todos, setTodos] = React.useState([]);
  const [shopping, setShopping] = React.useState([]);
  const [notes, setNotes] = React.useState([]);
  const [rules, setRules] = React.useState([]);
  const [family, setFamily] = React.useState(FALLBACK_FAMILY);
  const [geofences, setGeofences] = React.useState(FALLBACK_GEOFENCES);
  const [cameras] = React.useState(FALLBACK_CAMERAS);
  const [events, setEvents] = React.useState([]);
  const [waste, setWaste] = React.useState([]);
  const [wasteCalendar, setWasteCalendar] = React.useState([]); // full recycleapp.be collection list
  const [wasteConfig, setWasteConfig] = React.useState(null);
  const [activity, setActivity] = React.useState([]);
  const [trackerOffsets, setTrackerOffsets] = React.useState({});
  const [loading, setLoading] = React.useState(true);
  const devicesRef = React.useRef([]);
  const deviceStatePollBusyRef = React.useRef(false);
  const smartThingsAuthToastAtRef = React.useRef(0);

  // Notifications & toasts
  const [notifications, setNotifications] = React.useState([]);
  const [toasts, setToasts] = React.useState([]);
  const toastIdRef = React.useRef(0);
  const geofenceTimers = React.useRef({});
  const batteryTimers = React.useRef({});

  React.useEffect(() => {
    devicesRef.current = devices;
  }, [devices]);

  // Initial data fetch
  React.useEffect(() => {
    Promise.all([
      api('/devices').then(ds => setDevices(ds.map(normDevice))).catch(() => {}),
      api('/scenes').then(ss => setScenes(ss.map(normScene))).catch(() => {}),
      api('/todos').then(ts => setTodos(ts.map(normTodo))).catch(() => {}),
      api('/shopping').then(ss => setShopping(ss.map(normShop))).catch(() => {}),
      api('/notes').then(ns => setNotes(ns.map(normNote))).catch(() => {}),
      api('/rules').then(rs => setRules(rs.map(normRule))).catch(() => {}),
      api('/calendar').then(es => setEvents(es.map(normCalEvent))).catch(() => {}),
      api('/config/geofences').then(gs => setGeofences(gs.map(normGeofence))).catch(() => {}),
      api('/waste').then(ws => setWaste(ws.map(normWaste))).catch(() => {}),
      api('/waste/recycleapp').then(data => {
        setWasteCalendar(data.collections || []);
        setWasteConfig(data.config || null);
      }).catch(() => {}),
      api('/notifications').then(ns => setActivity(ns.slice(0, 50).map(normActivity))).catch(() => {}),
      api('/notifications').then(ns => setNotifications(ns)).catch(() => {}),
      api('/auth/users').then(us => setFamily(us.map(normUser))).catch(() => {}),
      api('/tracker/offsets').then(setTrackerOffsets).catch(() => {}),
    ]).finally(() => setLoading(false));
  }, []);

  React.useEffect(() => {
    if (loading) return;

    let cancelled = false;
    const syncRuntimeState = async () => {
      const snapshot = devicesRef.current.filter(shouldPollDeviceState);
      if (!snapshot.length || deviceStatePollBusyRef.current) return;
      deviceStatePollBusyRef.current = true;
      try {
        const results = await Promise.allSettled(
          snapshot.map(device =>
            api(`/devices/${device.id}/state`, { method: 'POST' })
              .then(result => ({ id: device.id, state: result?.state || null }))
          )
        );

        if (cancelled) return;

        const stateById = new Map();
        results.forEach((result) => {
          if (result.status !== 'fulfilled') return;
          if (!result.value?.state || typeof result.value.state !== 'object') return;
          stateById.set(String(result.value.id), result.value.state);
        });

        if (!stateById.size) return;
        setDevices(prev => prev.map(device => {
          const runtime = stateById.get(String(device.id));
          return runtime ? mergeDeviceRuntime(device, runtime) : device;
        }));
      } finally {
        deviceStatePollBusyRef.current = false;
      }
    };

    syncRuntimeState();
    const interval = setInterval(syncRuntimeState, 20000);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [loading, devices.map(device => `${device.id}:${device._raw?.integration || device.brand || ''}:${device.type}`).join('|')]);

  // Tracker loop — HTTP poll + WebSocket for real-time updates
  React.useEffect(() => {
    // Helper: apply a GeoJSON FeatureCollection to the family state
    const applyGeoFeatures = (features) => {
      if (!Array.isArray(features)) return;
      setFamily(prevFamily => {
        let updated = false;
        const nextFamily = prevFamily.map(f => {
          if (!f.tracker_id) return f;
          const feat = features.find(x =>
            (x.properties?.tid === f.tracker_id) ||
            (x.properties?.user === f.tracker_id)
          );
          if (!feat) return f;
          const [lng, lat] = feat.geometry.coordinates;
          const props = feat.properties;
          const battery = props.batt ?? props.battery ?? f.battery;
          const timestamp = props.tst ?? props.timestamp ?? f.timestamp;
          if (f.lat === lat && f.lng === lng && f.battery === battery) return f;
          updated = true;
          return {
            ...f, lat, lng, battery, tracked: true, timestamp,
            location: props.inregions?.[0] || props.loc || f.location || '',
          };
        });
        return updated ? nextFamily : prevFamily;
      });
    };

    // Helper: apply a single WS location message to one family member
    const applySingleLocation = (msg) => {
      if (!msg.user || msg.lat == null || msg.lon == null) return;
      setFamily(prevFamily => {
        const nextFamily = prevFamily.map(f => {
          if (f.tracker_id !== msg.user) return f;
          if (f.lat === msg.lat && f.lng === msg.lon) return f;
          return {
            ...f, lat: msg.lat, lng: msg.lon,
            battery: msg.batt ?? msg.battery ?? f.battery,
            tracked: true, timestamp: msg.tst ?? msg.timestamp ?? f.timestamp,
            location: msg.inregion || f.location || '',
          };
        });
        return nextFamily;
      });
    };

    // HTTP poll — seeds initial data and acts as WS fallback
    const fetchTracker = () => {
      fetch('/api/tracker/geojson').then(r => r.json()).then(geo => {
        applyGeoFeatures(geo.features);
      }).catch(() => {});
    };
    fetchTracker();
    const interval = setInterval(fetchTracker, 15000); // 15s fallback

    // WebSocket — real-time position streaming
    let ws, reconnectTimer;
    const connectWS = (wsUrl) => {
      if (!wsUrl) return;
      try {
        ws = new WebSocket(wsUrl);
        ws.onmessage = (e) => {
          try {
            const msg = JSON.parse(e.data);
            if (msg.type === 'location') applySingleLocation(msg);

            // ── Geofence enter/leave events ──
            if (msg.type === 'geofence' && msg.event === 'enter') {
              const key = `${msg.user}:${msg.zone}`;
              if (geofenceTimers.current[key]) clearTimeout(geofenceTimers.current[key]);
              geofenceTimers.current[key] = setTimeout(() => {
                delete geofenceTimers.current[key];
                const name = resolveTracker(msg.user);
                const zoneName = msg.zoneName || msg.zone || 'zone';
                addToast({ type: 'geofence', title: `${name} arrived at ${zoneName}`, message: `Entered ${zoneName}`, icon: 'geofence' });
                // Refresh notification list (server creates DB entry)
                api('/notifications').then(ns => setNotifications(ns)).catch(() => {});
              }, 30000); // 30s delay — cancelled if they leave quickly
            }
            if (msg.type === 'geofence' && msg.event === 'leave') {
              const key = `${msg.user}:${msg.zone}`;
              if (geofenceTimers.current[key]) {
                clearTimeout(geofenceTimers.current[key]);
                delete geofenceTimers.current[key];
              }
            }
            if (msg.type === 'geofence' && msg.event === 'leave_confirmed') {
              const name = resolveTracker(msg.user);
              const zoneName = msg.zoneName || msg.zone || 'zone';
              const dur = msg.durationSeconds;
              const durText = dur
                ? ` (was there ${dur >= 3600 ? `${Math.floor(dur / 3600)}h ${Math.floor((dur % 3600) / 60)}m` : dur >= 60 ? `${Math.floor(dur / 60)}m` : `${dur}s`})`
                : '';
              addToast({ type: 'geofence', title: `${name} left ${zoneName}`, message: `Left ${zoneName}${durText}`, icon: 'geofence' });
              api('/notifications').then(ns => setNotifications(ns)).catch(() => {});
            }

            // ── Battery low events ──
            if (msg.type === 'battery_low') {
              const key = `battery:${msg.user}`;
              if (batteryTimers.current[key]) return;
              batteryTimers.current[key] = true;
              setTimeout(() => { delete batteryTimers.current[key]; }, 60 * 60 * 1000); // 1h throttle
              const name = resolveTracker(msg.user);
              addToast({ type: 'warning', title: `${name}'s battery low`, message: `Battery at ${msg.battery}%`, icon: 'battery' });
            }
          } catch {}
        };
        ws.onclose = () => { reconnectTimer = setTimeout(() => connectWS(wsUrl), 5000); };
        ws.onerror = () => { try { ws.close(); } catch {} };
      } catch {}
    };
    // Fetch the WS URL from config and connect
    fetch('/api/config/public').then(r => r.json()).then(cfg => {
      if (cfg?.wsUrl) connectWS(cfg.wsUrl);
    }).catch(() => {});

    return () => {
      clearInterval(interval);
      clearTimeout(reconnectTimer);
      try { ws?.close(); } catch {}
      Object.values(geofenceTimers.current).forEach(clearTimeout);
    };
  }, []);

  // ── Browser geolocation — send live position every 2s ──────────────────────
  React.useEffect(() => {
    if (!navigator.geolocation) return;
    let lastSent = 0;
    const watchId = navigator.geolocation.watchPosition(
      (pos) => {
        const now = Date.now();
        if (now - lastSent < 2000) return; // throttle to every 2s
        lastSent = now;
        const payload = {
          lat: pos.coords.latitude,
          lon: pos.coords.longitude,
          acc: pos.coords.accuracy ? Math.round(pos.coords.accuracy) : null,
          alt: pos.coords.altitude,
          vel: pos.coords.speed,
          tst: Math.floor(now / 1000),
        };
        fetch('/api/tracker/location', {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        }).catch(() => {});
      },
      () => {}, // silently ignore errors
      { enableHighAccuracy: true, maximumAge: 1000, timeout: 5000 }
    );
    return () => navigator.geolocation.clearWatch(watchId);
  }, []);

  // Resolve tracker ID to human name
  const resolveTracker = (trackerId) => {
    const u = family.find(f => f.tracker_id === trackerId);
    return u ? u.name : trackerId;
  };

  // ── Toast helpers ──
  const addToast = (toast) => {
    const id = ++toastIdRef.current;
    setToasts(prev => [...prev, { ...toast, id }]);
    setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 6000);
  };
  const dismissToast = (id) => setToasts(prev => prev.filter(t => t.id !== id));

  const handleSmartThingsAuthIssue = (deviceId, error) => {
    const device = devicesRef.current.find(entry => String(entry.id) === String(deviceId));
    if (!device || device._raw?.integration !== 'smartthings') return;
    const message = String(error?.message || '');
    if (!/SmartThings|401|403|502/.test(message)) return;
    fetchSmartThingsStatus();
    const now = Date.now();
    if (now - smartThingsAuthToastAtRef.current < 10000) return;
    smartThingsAuthToastAtRef.current = now;
    addToast({
      type: 'warning',
      title: 'SmartThings token needs updating',
      message: 'Open Hubs & Bridges and paste a new Personal Access Token.',
      icon: 'wifi',
    });
  };

  // ── Notification helpers ──
  const fetchNotifications = () => api('/notifications').then(ns => setNotifications(ns)).catch(() => {});
  const markNotifRead = (id) => {
    api(`/notifications/${id}/read`, { method: 'PUT' })
      .then(() => setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: 1 } : n)))
      .catch(() => {});
  };
  const markAllNotifsRead = () => {
    api('/notifications/read-all', { method: 'PUT' })
      .then(() => setNotifications(prev => prev.map(n => ({ ...n, read: 1 }))))
      .catch(() => {});
  };
  const deleteNotif = (id) => {
    api(`/notifications/${id}`, { method: 'DELETE' })
      .then(() => setNotifications(prev => prev.filter(n => n.id !== id)))
      .catch(() => {});
  };

  // ── Devices ─────────────────────────────────────────────────
  const toggleDevice = (id) => {
    const d = devices.find(x => x.id === id);
    if (!d) return;
    const newOn = !d.on;
    setDevices(ds => ds.map(x => x.id === id ? { ...x, on: newOn } : x));
    return api(`/devices/${id}/control`, { method: 'POST', body: JSON.stringify({ action: newOn ? 'on' : 'off' }) })
      .then(r => {
        if (r && r.state) {
          setDevices(ds => ds.map(device => device.id === id ? mergeDeviceRuntime(device, r.state) : device));
        }
        return r;
      })
      .catch((error) => {
        handleSmartThingsAuthIssue(id, error);
        setDevices(ds => ds.map(x => x.id === id ? { ...x, on: !newOn } : x));
        throw error;
      });
  };

  const setDeviceValue = (id, v) => {
    setDevices(ds => ds.map(d => d.id === id ? { ...d, value: v, on: v > 0 } : d));
    return api(`/devices/${id}/control`, { method: 'POST', body: JSON.stringify({ action: 'brightness', value: v }) })
      .then(r => {
        if (r && r.state) {
          setDevices(ds => ds.map(device => device.id === id ? mergeDeviceRuntime(device, r.state) : device));
        }
        return r;
      })
      .catch(() => {});
  };

  const controlDevice = (id, action, value) => {
    return api(`/devices/${id}/control`, { method: 'POST', body: JSON.stringify({ action, value }) })
      .then(r => {
        if (r && r.state) {
          setDevices(ds => ds.map(d => d.id === id ? mergeDeviceRuntime(d, r.state) : d));
        }
        return r;
      })
      .catch((error) => {
        handleSmartThingsAuthIssue(id, error);
        throw error;
      });
  };

  const getDeviceState = (id) => {
    return api(`/devices/${id}/state`, { method: 'POST' })
      .then(r => {
        if (r && r.state) {
          setDevices(ds => ds.map(d => d.id === id ? mergeDeviceRuntime(d, r.state) : d));
        }
        return r;
      })
      .catch((error) => {
        handleSmartThingsAuthIssue(id, error);
        return null;
      });
  };

  const getDeviceSpec = (id) => {
    return api(`/devices/${id}/specification`).catch(() => null);
  };

  // ── Scenes ───────────────────────────────────────────────────
  const activateScene = (id) => {
    setScenes(ss => ss.map(s => ({ ...s, active: s.id === id })));
    api(`/scenes/${id}/activate`, { method: 'POST' }).catch(() => {});
  };

  // ── Todos ────────────────────────────────────────────────────
  const toggleTodo = (id) => {
    const t = todos.find(x => x.id === id);
    if (!t) return;
    const done = !t.done;
    setTodos(ts => ts.map(x => x.id === id ? { ...x, done } : x));
    api(`/todos/${id}`, { method: 'PUT', body: JSON.stringify({ done: done ? 1 : 0 }) }).catch(() => {
      setTodos(ts => ts.map(x => x.id === id ? { ...x, done: !done } : x));
    });
  };

  const addTodo = (text, assignee, dueDate) => {
    const tmp = { id: 'tmp_' + Date.now(), text, done: false, list: 'general', due: dueDate ? fmtDue(dueDate) : 'Today', assign: assignee || '' };
    setTodos(ts => [tmp, ...ts]);
    api('/todos', { method: 'POST', body: JSON.stringify({ title: text, assigned_to: assignee || undefined, due_date: dueDate || undefined }) })
      .then(t => setTodos(ts => ts.map(x => x.id === tmp.id ? normTodo(t) : x)))
      .catch(() => setTodos(ts => ts.filter(x => x.id !== tmp.id)));
  };

  const removeTodo = (id) => {
    setTodos(ts => ts.filter(t => t.id !== id));
    api(`/todos/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  // ── Shopping ─────────────────────────────────────────────────
  const toggleShop = (id) => {
    const s = shopping.find(x => x.id === id);
    if (!s) return;
    const checked = !s.done;
    setShopping(ss => ss.map(x => x.id === id ? { ...x, done: checked } : x));
    api(`/shopping/${id}`, { method: 'PUT', body: JSON.stringify({ checked: checked ? 1 : 0 }) }).catch(() => {
      setShopping(ss => ss.map(x => x.id === id ? { ...x, done: !checked } : x));
    });
  };

  const addShop = (text, qty = '', category = 'general') => {
    const tmp = { id: 'tmp_' + Date.now(), text, done: false, qty, cat: category, added: '' };
    setShopping(ss => [tmp, ...ss]);
    api('/shopping', { method: 'POST', body: JSON.stringify({ item: text, quantity: qty, category }) })
      .then(s => setShopping(ss => ss.map(x => x.id === tmp.id ? normShop(s) : x)))
      .catch(() => setShopping(ss => ss.filter(x => x.id !== tmp.id)));
  };

  const removeShop = (id) => {
    setShopping(ss => ss.filter(s => s.id !== id));
    api(`/shopping/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  const clearShopDone = () => {
    setShopping(ss => ss.filter(s => !s.done));
    api('/shopping', { method: 'DELETE' }).catch(() => {});
  };

  // ── Notes ────────────────────────────────────────────────────
  const addNote = (content, color = '#F0DCA8') => {
    const tmp = { id: 'tmp_' + Date.now(), ...normNote({ content, color, pinned: false }) };
    setNotes(ns => [tmp, ...ns]);
    api('/notes', { method: 'POST', body: JSON.stringify({ content, color }) })
      .then(n => setNotes(ns => ns.map(x => x.id === tmp.id ? normNote(n) : x)))
      .catch(() => setNotes(ns => ns.filter(n => n.id !== tmp.id)));
  };

  const removeNote = (id) => {
    setNotes(ns => ns.filter(n => n.id !== id));
    api(`/notes/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  const toggleNotePin = (id) => {
    const n = notes.find(x => x.id === id);
    if (!n) return;
    const pinned = !n.pinned;
    setNotes(ns => ns.map(x => x.id === id ? { ...x, pinned } : x));
    api(`/notes/${id}`, { method: 'PUT', body: JSON.stringify({ pinned: pinned ? 1 : 0 }) }).catch(() => {
      setNotes(ns => ns.map(x => x.id === id ? { ...x, pinned: !pinned } : x));
    });
  };

  const updateNote = (id, content, color) => {
    const patch = {};
    if (content !== undefined) patch.content = content;
    if (color !== undefined) patch.color = color;
    setNotes(ns => ns.map(x => x.id === id ? { ...x, ...(content !== undefined ? { title: content.split('\n')[0] || '—', body: content.split('\n').slice(1).join('\n') } : {}), ...(color !== undefined ? { color } : {}) } : x));
    api(`/notes/${id}`, { method: 'PUT', body: JSON.stringify(patch) }).catch(() => {});
  };

  // ── Calendar extras ──────────────────────────────────────────
  const removeEvent = (id) => {
    setEvents(es => es.filter(e => e.id !== id));
    api(`/calendar/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  const updateEvent = (id, payload) => {
    api(`/calendar/${id}`, { method: 'PUT', body: JSON.stringify(payload) })
      .then(updated => setEvents(es => es.map(e => e.id === id ? normCalEvent(updated) : e)))
      .catch(() => {});
  };

  // ── Waste ────────────────────────────────────────────────────
  const addWaste = (data) => {
    const tmpId = 'tmp_' + Date.now();
    const tmp = normWaste({ ...data, id: tmpId });
    setWaste(ws => [...ws, tmp]);
    api('/waste', { method: 'POST', body: JSON.stringify(data) })
      .then(w => setWaste(ws => ws.map(x => x.id === tmpId ? normWaste(w) : x)))
      .catch(() => setWaste(ws => ws.filter(w => w.id !== tmpId)));
  };

  const removeWaste = (id) => {
    setWaste(ws => ws.filter(w => String(w.id) !== String(id)));
    api(`/waste/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  const updateWaste = (id, patch) => {
    setWaste(ws => ws.map(w => String(w.id) === String(id) ? normWaste({ ...(w._raw || w), ...patch }) : w));
    api(`/waste/${id}`, { method: 'PUT', body: JSON.stringify(patch) })
      .then(w => setWaste(ws => ws.map(x => String(x.id) === String(id) ? normWaste(w) : x)))
      .catch(() => {});
  };

  const markWasteCollected = (id) => {
    api(`/waste/${id}/collected`, { method: 'POST' })
      .then(w => setWaste(ws => ws.map(x => String(x.id) === String(id) ? normWaste(w) : x)))
      .catch(() => {});
  };

  // ── RecycleApp.be ────────────────────────────────────────────
  const configureRecycleApp = (postal, street, number) => {
    return api('/waste/recycleapp/configure', { method: 'POST', body: JSON.stringify({ postal, street, number }) })
      .then(data => {
        setWasteCalendar(data.collections || []);
        setWasteConfig(data.config || null);
        return data;
      })
      .catch(() => {});
  };

  const syncRecycleApp = () => {
    return api('/waste/recycleapp/sync', { method: 'POST' })
      .then(data => {
        if (data.schedule) setWaste(data.schedule.map(normWaste));
        if (data.collections) setWasteCalendar(data.collections);
      })
      .catch(() => {});
  };

  const refreshWasteCalendar = () => {
    api('/waste/recycleapp').then(data => { setWasteCalendar(data.collections || []); setWasteConfig(data.config || null); }).catch(() => {});
  };

  const fetchTrackerOffsets = () => {
    return api('/tracker/offsets').then(offsets => {
      setTrackerOffsets(offsets || {});
      return offsets || {};
    }).catch(() => ({}));
  };

  const setTrackerOffset = (user, offset) => {
    return api(`/tracker/offsets/${encodeURIComponent(user)}`, { method: 'PUT', body: JSON.stringify(offset) })
      .then(res => {
        setTrackerOffsets(prev => ({ ...prev, [user]: res.offset }));
        return res;
      });
  };

  const clearTrackerOffset = (user) => {
    return api(`/tracker/offsets/${encodeURIComponent(user)}`, { method: 'DELETE' })
      .then(() => {
        setTrackerOffsets(prev => {
          const next = { ...prev };
          delete next[user];
          return next;
        });
      });
  };

  const fetchLocationHistory = (user, days = 7) => {
    const to = new Date();
    const from = new Date(Date.now() - days * 86400000);
    const params = new URLSearchParams({
      from: from.toISOString().slice(0, 10),
      to: to.toISOString().slice(0, 10),
    });
    return api(`/tracker/history/${encodeURIComponent(user)}?${params.toString()}`)
      .then(data => Array.isArray(data?.entries) ? data.entries : [])
      .catch(() => []);
  };

  // ── Scenes extras ────────────────────────────────────────────
  const createScene = (data) => {
    api('/scenes', { method: 'POST', body: JSON.stringify(data) })
      .then(s => setScenes(ss => [...ss, normScene(s)]))
      .catch(() => {});
  };

  const updateScene = (id, data) => {
    api(`/scenes/${id}`, { method: 'PUT', body: JSON.stringify(data) })
      .then(s => setScenes(ss => ss.map(x => x.id === id ? normScene(s) : x)))
      .catch(() => {});
  };

  const removeScene = (id) => {
    setScenes(ss => ss.filter(s => s.id !== id));
    api(`/scenes/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  // ── Devices extras ───────────────────────────────────────────
  const createDevice = (data) => {
    api('/devices', { method: 'POST', body: JSON.stringify(data) })
      .then(d => setDevices(ds => [...ds, normDevice(d)]))
      .catch(() => {});
  };

  const updateDeviceData = (id, data) => {
    api(`/devices/${id}`, { method: 'PUT', body: JSON.stringify(data) })
      .then(d => setDevices(ds => ds.map(x => x.id === id ? normDevice(d) : x)))
      .catch(() => {});
  };

  const removeDevice = (id) => {
    setDevices(ds => ds.filter(d => d.id !== id));
    api(`/devices/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  // ── Rules ────────────────────────────────────────────────────
  const toggleRule = (id) => {
    const r = rules.find(x => x.id === id);
    if (!r) return;
    const enabled = !r.active;
    setRules(rs => rs.map(x => x.id === id ? { ...x, active: enabled } : x));
    api(`/rules/${id}`, { method: 'PUT', body: JSON.stringify({ enabled }) }).catch(() => {
      setRules(rs => rs.map(x => x.id === id ? { ...x, active: !enabled } : x));
    });
  };

  const addRule = (text) => {
    const payload = typeof text === 'string'
      ? {
          name: text.slice(0, 80),
          description: text,
          conditions: [{ type: 'all_users_away', label: 'Nobody is home' }],
          actions: [{ type: 'notify', label: 'Send notification', title: 'Rule triggered', message: text }],
          cooldown_minutes: 5,
          enabled: true,
        }
      : {
          ...text,
          description: text.description || text.name || 'Smart rule',
        };
    const tmp = { id: 'tmp_' + Date.now(), ...normRule({ ...payload, id: 'tmp', run_count: 0, enabled: true }) };
    setRules(rs => [tmp, ...rs]);
    api('/rules', { method: 'POST', body: JSON.stringify(payload) })
      .then(r => setRules(rs => rs.map(x => x.id === tmp.id ? normRule(r) : x)))
      .catch(() => setRules(rs => rs.filter(x => x.id !== tmp.id)));
  };

  const removeRule = (id) => {
    setRules(rs => rs.filter(r => r.id !== id));
    api(`/rules/${id}`, { method: 'DELETE' }).catch(() => {});
  };

  const updateRule = (id, patch) => {
    api(`/rules/${id}`, { method: 'PUT', body: JSON.stringify(patch) })
      .then(r => setRules(rs => rs.map(x => x.id === id ? normRule(r) : x)))
      .catch(() => {});
  };

  // ── Calendar ─────────────────────────────────────────────────
  const addEvent = (payload) => {
    const tmpId = 'tmp_' + Date.now();
    const tmp = normCalEvent({ id: tmpId, ...payload });
    setEvents(es => [tmp, ...es]);
    api('/calendar', { method: 'POST', body: JSON.stringify(payload) })
      .then((created) => setEvents(es => es.map(e => e.id === tmpId ? normCalEvent(created) : e)))
      .catch(() => setEvents(es => es.filter(e => e.id !== tmpId)));
  };

  // ── Geofences ────────────────────────────────────────────────
  const syncGeofences = (next, prev = geofences) => {
    setGeofences(next);
    api('/config/geofences', { method: 'PUT', body: JSON.stringify(next.map(toApiGeofence)) })
      .catch(() => {
        setGeofences(prev);
        api('/config/geofences').then(gs => setGeofences(gs.map(normGeofence))).catch(() => {});
      });
  };

  const addGeofence = (zone) => {
    const existingNums = geofences
      .map(g => Number(g.id))
      .filter(n => Number.isFinite(n));
    const nextId = Number.isFinite(Number(zone.id))
      ? Number(zone.id)
      : ((existingNums.length ? Math.max(...existingNums) : 0) + 1);

    const newFence = {
      id: String(nextId),
      name: zone.name || 'New zone',
      lat: Number(zone.lat) || 0,
      lng: Number(zone.lng) || 0,
      radius: Number(zone.radius) || 120,
      color: zone.color || '#3b82f6',
      members: Array.isArray(zone.members) ? zone.members.map(String) : [],
      notify: zone.notify !== false,
      icon: zone.icon || 'pin',
      address: zone.address || '',
    };

    // Optimistic update
    setGeofences(prev => [newFence, ...prev]);

    // POST single zone — safer than PUT-all which could wipe fences on stale state
    api('/config/geofences', {
      method: 'POST',
      body: JSON.stringify({ id: newFence.id, name: newFence.name, lat: newFence.lat, lon: newFence.lng, radius: newFence.radius, color: newFence.color, icon: newFence.icon, notify: newFence.notify }),
    }).then(r => {
      // Server assigned the canonical id — update local state
      if (r?.id && r.id !== newFence.id) {
        setGeofences(prev => prev.map(g => g.id === newFence.id ? { ...g, id: String(r.id) } : g));
      }
    }).catch(() => {
      // Rollback
      setGeofences(prev => prev.filter(g => g.id !== newFence.id));
    });
  };

  const removeGeofence = (id) => {
    const prev = geofences;
    setGeofences(gs => gs.filter(g => g.id !== id));
    api(`/config/geofences/${id}`, { method: 'DELETE' })
      .catch(() => setGeofences(prev));
  };

  const updateGeofence = (id, patch) => {
    const prev = geofences;
    setGeofences(gs => gs.map(g => g.id === id ? { ...g, ...patch } : g));
    // Convert lng → lon for the server
    const body = { ...patch };
    if (body.lng !== undefined) { body.lon = body.lng; delete body.lng; }
    api(`/config/geofences/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
      .catch(() => setGeofences(prev));
  };

  // ── Device discovery ─────────────────────────────────────────
  const discoverDevices = () => {
    return api('/devices/discover').catch(() => ({ devices: [], errors: [] }));
  };

  // ── Device timers/schedules ──────────────────────────────────
  const listTimers = (id) => api(`/devices/${id}/timers`).catch(() => ({ timers: [] }));

  const createTimer = (id, payload) => {
    return api(`/devices/${id}/timers`, { method: 'POST', body: JSON.stringify(payload) });
  };

  const deleteTimer = (deviceId, groupId) => {
    return api(`/devices/${deviceId}/timers/${groupId}`, { method: 'DELETE' });
  };

  // ── Camera stream allocation ─────────────────────────────────
  const allocateStream = (id) => {
    return api(`/devices/${id}/stream`, { method: 'POST', body: JSON.stringify({ type: 'hls' }) });
  };

  // ── Room control (bulk on/off) ───────────────────────────────
  const controlRoom = (room, action, value) => {
    return api('/devices/control-room', { method: 'POST', body: JSON.stringify({ room, action, value }) })
      .then(results => {
        if (Array.isArray(results)) {
          setDevices(ds => ds.map(device => {
            const result = results.find(entry => String(entry.id) === String(device.id));
            if (!result) return device;
            if (result.state) return mergeDeviceRuntime(device, result.state);
            if (!result.error && (action === 'on' || action === 'off')) return { ...device, on: action === 'on' };
            return device;
          }));
        }
        return results;
      });
  };

  // ── Weather ──────────────────────────────────────────────────
  const [weather, setWeather] = React.useState(null);
  React.useEffect(() => {
    const WMO = {0:'Clear',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Heavy drizzle',61:'Light rain',63:'Rain',65:'Heavy rain',71:'Light snow',73:'Snow',75:'Heavy snow',80:'Light showers',81:'Showers',82:'Heavy showers',95:'Thunderstorm',96:'Thunderstorm + hail',99:'Heavy thunderstorm'};
    const fetchWeather = () => {
      fetch('https://api.open-meteo.com/v1/forecast?latitude=50.98146&longitude=4.572189&current=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m&timezone=Europe/Brussels')
        .then(r => r.json())
        .then(d => {
          if (!d.current) return;
          const code = d.current.weather_code;
          let icon = '☀️';
          if (code >= 95) icon = '⛈';
          else if (code >= 71) icon = '❄️';
          else if (code >= 51) icon = '🌧';
          else if (code >= 45) icon = '🌫';
          else if (code >= 2) icon = '☁️';
          else if (code >= 1) icon = '⛅';
          setWeather({
            temp: Math.round(d.current.temperature_2m),
            icon,
            label: WMO[code] || 'Unknown',
            wind: Math.round(d.current.wind_speed_10m),
            humidity: d.current.relative_humidity_2m,
          });
        })
        .catch(() => {});
    };
    fetchWeather();
    const iv = setInterval(fetchWeather, 15 * 60 * 1000);
    return () => clearInterval(iv);
  }, []);

  // ── Web Push ─────────────────────────────────────────────────
  const [pushEnabled, setPushEnabled] = React.useState(false);
  const [pushBusy, setPushBusy] = React.useState(false);
  const [pushAvailable, setPushAvailable] = React.useState(false);
  const [pushStatus, setPushStatus] = React.useState('Checking…');

  // Check push state on mount
  React.useEffect(() => {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
      setPushAvailable(false);
      setPushStatus('This browser does not support push notifications');
      return;
    }

    api('/notifications/push/public-key').then(({ enabled, publicKey }) => {
      setPushAvailable(!!enabled && !!publicKey);
      setPushStatus(enabled && publicKey ? 'Available' : 'Server push is not configured');
      return navigator.serviceWorker.getRegistration();
    }).then(reg => {
      if (!reg) return null;
      return reg.pushManager.getSubscription();
    }).then(sub => {
      if (sub) {
        setPushEnabled(true);
        setPushStatus('Enabled on this device');
      }
    }).catch(() => {
      setPushAvailable(false);
      setPushStatus('Push status could not be checked');
    });
  }, []);

  const urlBase64ToUint8Array = (base64String) => {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
    const raw = atob(base64);
    return Uint8Array.from([...raw].map(c => c.charCodeAt(0)));
  };

  const togglePush = async () => {
    if (!('Notification' in window) || !('serviceWorker' in navigator)) return;
    setPushBusy(true);
    try {
      if (pushEnabled) {
        // Disable
        const reg = await navigator.serviceWorker.getRegistration();
        const sub = reg && await reg.pushManager.getSubscription();
        if (sub) {
          await api('/notifications/push/unsubscribe', { method: 'POST', body: JSON.stringify({ endpoint: sub.endpoint }) }).catch(() => {});
          await sub.unsubscribe();
        }
        setPushEnabled(false);
        setPushStatus('Disabled on this device');
      } else {
        // Enable
        const perm = await Notification.requestPermission();
        if (perm !== 'granted') {
          setPushStatus('Notification permission was denied');
          setPushBusy(false);
          return;
        }
        const { publicKey, enabled } = await api('/notifications/push/public-key');
        if (!enabled || !publicKey) {
          setPushAvailable(false);
          setPushStatus('Server push is not configured');
          setPushBusy(false);
          return;
        }
        const reg = await navigator.serviceWorker.register('/sw.js');
        const sub = await reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(publicKey),
        });
        await api('/notifications/push/subscribe', { method: 'POST', body: JSON.stringify(sub) });
        setPushEnabled(true);
        setPushAvailable(true);
        setPushStatus('Enabled on this device');
        // Test
        api('/notifications/push/test', { method: 'POST' }).catch(() => {});
      }
    } catch (e) {
      console.error('Push toggle failed:', e);
      setPushStatus(e.message || 'Push setup failed');
    }
    setPushBusy(false);
  };

  const [smartThingsStatus, setSmartThingsStatus] = React.useState(null);

  const fetchSmartThingsStatus = () => {
    return api('/config/smartthings/status').then(status => {
      setSmartThingsStatus(status);
      return status;
    }).catch((error) => {
      const fallback = { configured: false, valid: false, error: error.message };
      setSmartThingsStatus(fallback);
      return fallback;
    });
  };

  const updateSmartThingsToken = (token) => {
    return api('/config/smartthings/token', { method: 'PUT', body: JSON.stringify({ token }) })
      .then((status) => {
        setSmartThingsStatus(status);
        return fetchSmartThingsStatus();
      });
  };

  const updateFamilyMember = (id, patch) => {
    api(`/auth/users/${id}`, { method: 'PUT', body: JSON.stringify(patch) })
      .then(updated => setFamily(fs => fs.map(f => String(f.id) === String(id) ? { ...f, tracker_id: updated.tracker_id, role: updated.role } : f)))
      .catch(() => {});
  };

  return {
    loading,
    devices, toggleDevice, setDeviceValue, controlDevice, getDeviceState, getDeviceSpec, createDevice, updateDeviceData, removeDevice,
    discoverDevices, listTimers, createTimer, deleteTimer, allocateStream, controlRoom,
    scenes, activateScene, createScene, updateScene, removeScene,
    todos, toggleTodo, addTodo, removeTodo,
    shopping, toggleShop, addShop, removeShop, clearShopDone,
    notes, setNotes, addNote, removeNote, toggleNotePin, updateNote,
    rules, toggleRule, addRule, removeRule, updateRule,
    family, updateFamilyMember, geofences, addGeofence, updateGeofence, removeGeofence, cameras,
    events, addEvent, removeEvent, updateEvent,
    waste, addWaste, removeWaste, updateWaste, markWasteCollected,
    wasteCalendar, wasteConfig, configureRecycleApp, syncRecycleApp, refreshWasteCalendar,
    activity, weather, trackerOffsets, fetchTrackerOffsets, setTrackerOffset, clearTrackerOffset, fetchLocationHistory,
    // Notifications & toasts
    notifications, toasts, addToast, dismissToast,
    markNotifRead, markAllNotifsRead, deleteNotif, fetchNotifications,
    // Push
    pushEnabled, pushBusy, pushAvailable, pushStatus, togglePush,
    // SmartThings
    smartThingsStatus, fetchSmartThingsStatus, updateSmartThingsToken,
  };
}

window.useStore = useStore;

