/* Bee Smart Operations Portal - vNext canon prototype: LIVE fleet data.
   SITES + DEVICES are fetched at runtime from /api/fleet-snapshot (the real Next
   data layer: DATA_SOURCE=grafana -> Grafana proxy -> InfluxDB, adapted to this
   flat shape by lib/data/canon-adapter.ts). NO fabricated values: anything the
   portal does not collect is `null` and renders `-` in the UI. (s362, PM-191 --
   replaces the baked 2026-06-06 InfluxDB snapshot with live data; the app boots
   through loading/empty/error states while the fetch resolves.)

   NOT collected by the portal today (-> null -> `-`), each owned by a tracked PM:
     rssi/Signal (PM-160) · firmware (PM-175) · acres/crop/region (no tenant-meta store)
     · AQI/air-quality (gas is frozen, PM-180) · per-card integration health (PM-159) */

// Production flag: when this canon is served as the live portal (production
// index.html sets window.PORTAL_PROD=true; or preview with ?prod=1), the
// prototype-only chrome (the vNext ribbon + the per-page ReviewNote callouts)
// is hidden. Everything else renders pixel-identical. The canon + /next leave
// the flag unset so the sign-off chrome stays visible.
const PROD_PORTAL = (typeof window !== 'undefined') && (
  window.PORTAL_PROD === true ||
  /[?&]prod=1/.test((typeof location !== 'undefined' && location.search) || '') ||
  // auto: served as the production ROOT (not the /next sign-off surface, not localhost)
  (typeof location !== 'undefined' && location.hostname === 'portal.tbdcloud.com' && !location.pathname.startsWith('/next'))
);

// Production root keeps the static neutral tab title ("Bee Smart Operations"); the
// /next sign-off surface + local preview re-label themselves as the canon prototype.
if (typeof document !== 'undefined' && !PROD_PORTAL) document.title = 'Bee Smart Operations - vNext Canon';

// Deployments (the kit calls these SITES; each is one grower/retail customer +
// site). Populated by loadFleet() from /api/fleet-snapshot; empty until then.
let SITES = [];

function fmtAgo(min){
  if(min==null) return '-';
  if(min<1) return 'just now'; if(min<60) return min+'m ago';
  const h=Math.floor(min/60); if(h<24) return h+'h ago'; return Math.floor(h/24)+'d ago';
}

// Real fleet, fetched live from /api/fleet-snapshot (lib/data -> InfluxDB, adapted
// by canon-adapter.ts). reading.gas is the MOhm magnitude; air-quality (aqi) stays
// `-` until PM-180 (frozen gas) + PM-004 (IAQ) land. reading.noise IS live but the
// UI renders it per the s353 plan default. Empty until loadFleet() resolves.
let DEVICES = [];

// Fleet-wide telemetry count over the trailing 24h (canon-adapter readings24h =
// sum of the listSeries window counts). null until loadFleet() resolves / on a
// degraded series query -> the Overview "Readings (24h)" card renders "-".
let READINGS24H = null;

// Per-30m-bucket fleet trend over -24h (canon-adapter `series`): [{messages, devices}].
// Feeds the Overview KPI sparklines. Empty until loadFleet() resolves / on a degraded
// series -> the sparklines render a flat baseline.
let SERIES = [];

const siteName = id => (SITES.find(s=>s.id===id)||{}).name || id;
const siteOf   = id => SITES.find(s=>s.id===id);

function counts(){
  const by={ gateway:{on:0,t:0}, router:{on:0,t:0}, sensor:{on:0,t:0} };
  DEVICES.forEach(d=>{ by[d.kind].t++; if(d.status==='online') by[d.kind].on++; });
  return by;
}
// COUNTS + onlineCount are (re)computed by publish() once DEVICES is populated.

// Alerts are DERIVED from the real snapshot (deriveAlerts; the production rule). Any
// device that is offline raises a Critical alert; any device at <=25% battery raises a
// Warning. Right now the only real condition is G2 (retail standalone) being dark -> one
// active Critical. The G1 mesh is fresh and its batteries are healthy (48/52/96%), so it
// raises nothing. No fabricated feed -- every alert maps to a real device state.
function deriveAlerts(){
  const out=[];
  DEVICES.filter(d=>d.status==='offline').forEach(d=>out.push({ id:'off-'+d.id, severity:'critical', status:'active',
    title:d.name+' offline', detail:'No telemetry from '+d.name+' at '+siteName(d.site)+' (last seen '+fmtAgo(d.lastSeen)+').', site:d.site, node:d.node }));
  DEVICES.filter(d=>d.battery!=null&&d.battery>0&&d.battery<=25).forEach(d=>out.push({ id:'low-'+d.id, severity:'warning', status:'active',
    title:d.name+' battery low ('+d.battery+'%)', detail:d.name+' at '+siteName(d.site)+' is at '+d.battery+'%.', site:d.site, node:d.node }));
  return out;
}
// ALERTS + activeAlerts are (re)computed by publish() once DEVICES is populated.

// Real integrations behind this deployment = the PLATFORM. status: 'up'|'degraded'|'down'|
// null(not probed). We can VERIFY the live data path right now -- this portal is served by
// Caddy on the UpCloud VM and this snapshot was pulled THROUGH that VM from InfluxDB with
// Grafana proxying it -- so UpCloud / InfluxDB / Grafana are known up at snapshot time.
// Soracom (LTE/SIM) and nRF Cloud are not actively probed yet -> null -> '-' until live
// per-component health lands (PM-159). No fake "Operational" for the un-probed ones.
const INTEGRATIONS = [
  { id:'upcloud',  name:'UpCloud',  role:'Cloud Infrastructure (VM 85.9.197.106)', status:'up',  note:'Verified - serving this portal', icon:'server',      url:'https://hub.upcloud.com' },
  { id:'influxdb', name:'InfluxDB', role:'Time-Series Telemetry Store',            status:'up',  note:'Verified - snapshot source',     icon:'database',    url:null },
  { id:'grafana',  name:'Grafana',  role:'Metrics & Dashboards',                   status:'up',  note:'Verified - proxy live',          icon:'bar-chart-3', url:'https://dashboards.tbdcloud.com' },
  { id:'nrf',      name:'nRF Cloud', role:'Device Firmware & DFU',                 status:null,  note:null,                             icon:'cpu',         url:'https://nrfcloud.com' },
  { id:'soracom',  name:'Soracom',  role:'LTE / SIM Management',                   status:null,  note:null,                             icon:'signal',      url:'https://console.soracom.io' },
];

// Platform status = the worst per-component health. Any 'down' -> red Outage; any 'degraded'
// -> amber Degraded; otherwise green Operational. Un-probed (null) components never block
// green (verified-up, green-if-none-down -- s358 owner ruling).
function derivePlatform(){
  const st=INTEGRATIONS.map(i=>i.status);
  if(st.includes('down'))     return { state:'down',     label:'systems down',          tone:'crit' };
  if(st.includes('degraded')) return { state:'degraded', label:'systems degraded',      tone:'warn' };
  return                             { state:'up',       label:'all systems nominal',   tone:'ok' };
}
const PLATFORM = derivePlatform();

// A site with no router AND no sensors is a standalone gateway (retail) -- no real mesh, so
// its "mesh path" is just Gateway -> Cloud and the "Mesh Path" label is dropped (s358).
const isStandalone = id => { const ds=DEVICES.filter(d=>d.site===id); return !ds.some(d=>d.kind==='router')&&!ds.some(d=>d.kind==='sensor'); };

// --- Live data boot --------------------------------------------------------
// The static prototype baked SITES/DEVICES in; the production canon fetches them
// from /api/fleet-snapshot. In production Caddy proxies /api/* -> the Next app on
// :3001, so the path is same-origin. For local preview the static files and the
// Next dev API live on different origins, so an apiBase override is supported:
//   window.FLEET_API_BASE = 'http://localhost:3000'  or  ?apiBase=http://localhost:3000
function fleetApiBase(){
  if(typeof window!=='undefined' && window.FLEET_API_BASE) return window.FLEET_API_BASE;
  const m=/[?&]apiBase=([^&]+)/.exec((typeof location!=='undefined' && location.search) || '');
  return m ? decodeURIComponent(m[1]) : '';
}

// Publish the static + derived globals with the CURRENT SITES/DEVICES. Called once
// up front with safe empty defaults (so any render before the fetch resolves never
// hits an undefined global) and again by loadFleet() after live data lands.
function publish(){
  const ALERTS = deriveAlerts();
  Object.assign(window, {
    SITES, DEVICES, READINGS24H, SERIES,
    COUNTS: counts(),
    onlineCount: DEVICES.filter(d=>d.status==='online').length,
    ALERTS,
    activeAlerts: ALERTS.filter(a=>a.status==='active'),
    siteName, siteOf, fmtAgo, INTEGRATIONS, PLATFORM, isStandalone,
  });
}
publish();

// Fetch live SITES + DEVICES. Resolves on success (app.jsx flips to 'ready');
// throws on a non-2xx / network failure (app.jsx shows the error state + retry).
window.loadFleet = async function(){
  const res = await fetch(fleetApiBase()+'/api/fleet-snapshot', { cache:'no-store' });
  if(!res.ok) throw new Error('fleet-snapshot HTTP '+res.status);
  const data = await res.json();
  SITES   = Array.isArray(data.sites)   ? data.sites   : [];
  DEVICES = Array.isArray(data.devices) ? data.devices : [];
  READINGS24H = (typeof data.readings24h === 'number') ? data.readings24h : null;
  SERIES = Array.isArray(data.series) ? data.series : [];
  publish();
  return data;
};

// Admin-gated grower edit (PM-184). PATCHes /api/growers/[id] (id = customer_id) with
// the operator's admin token (localStorage; never persisted server-side from here).
// Throws Error with .code=<status> (401 on a bad token -> the modal re-prompts).
// Same fleetApiBase() origin rule as loadFleet (same-origin in prod).
window.patchGrower = async function(id, fields, token){
  const res = await fetch(fleetApiBase()+'/api/growers/'+encodeURIComponent(id), {
    method:'PATCH',
    headers:{ 'Content-Type':'application/json', 'x-admin-token': token },
    body: JSON.stringify(fields),
  });
  if(!res.ok){ const e=new Error('grower PATCH HTTP '+res.status); e.code=res.status; throw e; }
  return res.json();
};
