(() => {
  "use strict";

  const CACHE_KEY = "nereus.telemetry.v1";
  const state = {
    buoys: [],
    readings: [],
    markers: [],
    responses: [],
    totalErrors: 0,
    connectivity: "offline",
    version: null,
    lastDataAt: null,
    healthPolling: false,
    dataPolling: false,
    probes: {
      liveness: {ok: null, changedAt: null},
      readiness: {ok: null, changedAt: null}
    }
  };

  const elements = Object.fromEntries([
    "clock", "version-value", "version-note", "liveness-dot", "liveness-time",
    "readiness-dot", "readiness-time", "error-total", "error-rate", "error-state",
    "buoy-count", "data-age", "map-state", "telemetry-state", "latest-temp",
    "latest-wave", "latest-salinity", "readings-chart"
  ].map((id) => [id, document.getElementById(id)]));

  const map = L.map("map", {zoomControl: false, attributionControl: true, scrollWheelZoom: false}).setView([39.3, -4.2], 5);
  map.attributionControl.addAttribution("Natural Earth");
  if (window.NEREUS_LAND) {
    L.geoJSON(window.NEREUS_LAND, {
      interactive: false,
      style: {className: "landmass", fillColor: "#183640", fillOpacity: .72, color: "#41636a", weight: .7}
    }).addTo(map);
  }

  function restoreCache() {
    try {
      const cached = JSON.parse(localStorage.getItem(CACHE_KEY));
      if (!cached || !Array.isArray(cached.buoys) || !Array.isArray(cached.readings)) return;
      state.buoys = cached.buoys;
      state.readings = cached.readings;
      state.lastDataAt = cached.savedAt || null;
      renderTelemetry(true);
    } catch (_) {
      // A damaged cache is equivalent to no cache and should stay invisible.
    }
  }

  function saveCache() {
    try {
      localStorage.setItem(CACHE_KEY, JSON.stringify({buoys: state.buoys, readings: state.readings, savedAt: state.lastDataAt}));
    } catch (_) {
      // Private browsing and storage quotas must not affect the dashboard.
    }
  }

  async function apiFetch(path) {
    const startedAt = Date.now();
    try {
      const response = await fetch(path, {headers: {Accept: "application/json"}, cache: "no-store"});
      state.responses.push({at: startedAt, error: !response.ok});
      if (!response.ok) {
        state.totalErrors += 1;
        throw Object.assign(new Error("API response was not successful"), {httpFailure: true});
      }
      return response.json();
    } catch (error) {
      if (!error.httpFailure) state.connectivity = "offline";
      throw error;
    } finally {
      renderErrors();
    }
  }

  async function probe(path, includeVersion = false) {
    try {
      const response = await fetch(path, {cache: "no-store"});
      return {ok: response.status === 200, version: includeVersion ? response.headers.get("X-Nereus-Version") : null};
    } catch (_) {
      return {ok: false, version: null};
    }
  }

  function updateProbe(name, ok) {
    const probeState = state.probes[name];
    if (probeState.ok !== ok) {
      probeState.ok = ok;
      probeState.changedAt = new Date();
    }
    elements[`${name}-dot`].className = `indicator ${ok ? "ok" : "bad"}`;
    elements[`${name}-time`].textContent = probeState.changedAt ? `Changed ${formatTime(probeState.changedAt)}` : "No transition yet";
  }

  async function pollHealth() {
    if (state.healthPolling) return;
    state.healthPolling = true;
    try {
      const [live, ready] = await Promise.all([probe("/healthz", true), probe("/readyz")]);
      if (live.version) state.version = live.version;
      updateProbe("liveness", live.ok);
      updateProbe("readiness", ready.ok);
      if (!live.ok) state.connectivity = "offline";
      renderConnection();
    } finally {
      state.healthPolling = false;
    }
  }

  async function pollData() {
    if (state.dataPolling) return;
    state.dataPolling = true;
    try {
      const [buoys, readings] = await Promise.all([
        apiFetch("/api/v1/buoys"),
        apiFetch("/api/v1/readings?limit=500")
      ]);
      state.buoys = Array.isArray(buoys) ? buoys : [];
      state.readings = Array.isArray(readings) ? readings : [];
      state.lastDataAt = new Date().toISOString();
      state.connectivity = "online";
      saveCache();
      renderTelemetry(false);
    } catch (error) {
      if (error.httpFailure) state.connectivity = "degraded";
      renderTelemetry(true);
    } finally {
      state.dataPolling = false;
      renderConnection();
    }
  }

  function renderConnection() {
    const online = state.connectivity === "online";
    const reachable = state.connectivity !== "offline";
    elements["telemetry-state"].className = `connection-pill ${state.connectivity}`;
    elements["telemetry-state"].textContent = state.connectivity === "online" ? "Live" : state.connectivity === "degraded" ? "API errors" : "Offline";
    elements["version-value"].textContent = reachable ? state.version || "unavailable" : "offline";
    elements["version-note"].textContent = reachable ? (state.version ? "Reported by API" : "Version unavailable") : "Cluster unavailable";
    document.querySelector(".map-card").classList.toggle("stale", !online);
    document.querySelector(".telemetry-card").classList.toggle("stale", !online);
    renderErrors();
  }

  function renderErrors() {
    const cutoff = Date.now() - 60000;
    state.responses = state.responses.filter((event) => event.at >= cutoff);
    const failed = state.responses.filter((event) => event.error).length;
    const rate = state.responses.length ? failed / state.responses.length : 0;
    elements["error-total"].textContent = state.totalErrors.toLocaleString();
    elements["error-rate"].textContent = `${(rate * 100).toFixed(1)}%`;
    elements["error-state"].textContent = state.connectivity === "offline"
      ? "API unreachable"
      : failed > 0 ? "API responding with errors" : "API responses healthy";
  }

  function renderTelemetry(stale) {
    renderMap(stale);
    drawChart();
    const latest = [...state.readings].sort((a, b) => new Date(b.recorded_at) - new Date(a.recorded_at))[0];
    elements["latest-temp"].textContent = numberOrDash(latest?.water_temp, 1);
    elements["latest-wave"].textContent = numberOrDash(latest?.wave_height, 1);
    elements["latest-salinity"].textContent = numberOrDash(latest?.salinity, 1);
    elements["buoy-count"].textContent = `${state.buoys.length} ${state.buoys.length === 1 ? "buoy" : "buoys"}`;
    elements["data-age"].textContent = state.lastDataAt ? `Updated ${formatTime(new Date(state.lastDataAt))}` : "No telemetry";
    elements["map-state"].textContent = state.buoys.length ? (stale ? "Showing last known positions" : "Live positions") : "Waiting for buoy positions";
  }

  function renderMap() {
    state.markers.forEach((marker) => marker.remove());
    const latest = new Map();
    state.readings.forEach((reading) => {
      const previous = latest.get(reading.buoy_id);
      if (!previous || new Date(reading.recorded_at) > new Date(previous.recorded_at)) latest.set(reading.buoy_id, reading);
    });
    state.markers = state.buoys.filter((buoy) => Number.isFinite(buoy.latitude) && Number.isFinite(buoy.longitude)).map((buoy) => {
      const reading = latest.get(buoy.id);
      const details = reading ? `${numberOrDash(reading.water_temp, 1)} °C · ${numberOrDash(reading.wave_height, 1)} m · ${numberOrDash(reading.salinity, 1)} PSU<br><small>${formatTime(new Date(reading.recorded_at))}</small>` : "Awaiting first sample";
      const icon = L.divIcon({className: "", html: '<div class="buoy-marker"></div>', iconSize: [13, 13], iconAnchor: [6, 6]});
      return L.marker([buoy.latitude, buoy.longitude], {icon}).bindTooltip(`<strong>${escapeText(buoy.name)}</strong><br>${buoy.latitude.toFixed(2)}, ${buoy.longitude.toFixed(2)}<br>${details}`, {direction: "top"}).addTo(map);
    });
    if (state.markers.length) {
      const bounds = L.featureGroup(state.markers).getBounds();
      if (bounds.isValid()) map.fitBounds(bounds.pad(.22), {maxZoom: 7, animate: false});
    }
  }

  function drawChart() {
    const canvas = elements["readings-chart"];
    const rect = canvas.getBoundingClientRect();
    const ratio = window.devicePixelRatio || 1;
    canvas.width = Math.max(1, Math.floor(rect.width * ratio));
    canvas.height = Math.max(1, Math.floor(rect.height * ratio));
    const context = canvas.getContext("2d");
    context.scale(ratio, ratio);
    const width = rect.width, height = rect.height, pad = {top: 13, right: 13, bottom: 23, left: 34};
    const values = [...state.readings].sort((a, b) => new Date(a.recorded_at) - new Date(b.recorded_at)).slice(-45);
    context.clearRect(0, 0, width, height);
    context.strokeStyle = "rgba(121, 173, 180, .12)";
    context.lineWidth = 1;
    for (let i = 0; i < 5; i += 1) {
      const y = pad.top + ((height - pad.top - pad.bottom) * i / 4);
      context.beginPath(); context.moveTo(pad.left, y); context.lineTo(width - pad.right, y); context.stroke();
    }
    if (values.length < 2) {
      context.fillStyle = "#789aa1"; context.font = "11px system-ui"; context.textAlign = "center";
      context.fillText("Telemetry will appear when the cluster is online", width / 2, height / 2);
      return;
    }
    const plot = (field, color, min, max) => {
      const spread = max - min;
      context.beginPath(); context.strokeStyle = color; context.lineWidth = 2; context.lineJoin = "round";
      values.forEach((item, index) => {
        const x = pad.left + ((width - pad.left - pad.right) * index / (values.length - 1));
        const y = pad.top + (height - pad.top - pad.bottom) * (1 - (Number(item[field]) - min) / spread);
        if (index === 0) context.moveTo(x, y); else context.lineTo(x, y);
      });
      context.stroke();
    };
    plot("water_temp", "#55e4d7", 10, 30);
    plot("wave_height", "#55a9ff", 0, 6);
    context.fillStyle = "#668d95"; context.font = "9px system-ui"; context.textAlign = "left";
    context.fillText(formatTime(new Date(values[0].recorded_at)), pad.left, height - 5);
    context.textAlign = "right"; context.fillText(formatTime(new Date(values.at(-1).recorded_at)), width - pad.right, height - 5);
  }

  function formatTime(date) { return new Intl.DateTimeFormat(undefined, {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(date); }
  function numberOrDash(value, digits) { return Number.isFinite(Number(value)) ? Number(value).toFixed(digits) : "--"; }
  function escapeText(value) { const node = document.createElement("span"); node.textContent = String(value || "Unnamed buoy"); return node.innerHTML; }

  restoreCache();
  renderConnection();
  pollHealth();
  pollData();
  setInterval(pollHealth, 2000);
  setInterval(pollData, 5000);
  setInterval(renderErrors, 1000);
  setInterval(() => { elements.clock.textContent = formatTime(new Date()); }, 1000);
  elements.clock.textContent = formatTime(new Date());
  window.addEventListener("resize", () => { map.invalidateSize(false); drawChart(); });
})();
