// workshop inventory editor. Vanilla JS, no framework, no build step — same choice as
// pantry-vision/frontend and the canvas SDKs.
//
// Config comes from URL query params (?api=...&token=...), never hardcoded: this file
// is a generic static asset served read-only, with no secret in it.
//
// WHY THIS PAGE EXISTS AT ALL, given the assistant can write the same rows: the
// inventory is the one table that describes *physical reality* — where a thing is,
// whether it is still on the shelf — and physical reality drifts without telling
// anybody. Every other table here records something that was said or decided, and
// those are only ever wrong if someone recorded them wrong. This one goes stale by
// itself, so it needs somewhere a person can sit down and fix it.
"use strict";

const params = new URLSearchParams(location.search);
const API = (params.get("api") || "").replace(/\/$/, "");
const TOKEN = params.get("token") || "";

if (!API || !TOKEN) {
  document.body.innerHTML =
    '<p class="error" style="padding:24px">workshop not configured — missing ?api=&token= in the URL.</p>';
  throw new Error("workshop: missing ?api=/&token= query params");
}

const $ = (id) => document.getElementById(id);

const STATUS_LABELS = {
  available: "Available",
  assigned: "Reserved",
  in_use: "In use",
  retired: "Retired",
};

// The sentence under each status, shown on the row. Reserved and in-use look similar
// in a list and mean very different things when you are deciding whether you can grab
// something right now, so the list says which out loud rather than relying on a colour.
const STATUS_HINTS = {
  available: "on the shelf, unpromised",
  assigned: "spoken for, but still on the shelf",
  in_use: "installed and working — taking it back means taking something apart",
  retired: "dead, sold or given away",
};

function api(path, options) {
  options = options || {};
  options.headers = Object.assign({ Authorization: `Bearer ${TOKEN}` }, options.headers || {});
  return fetch(`${API}${path}`, options).then((res) =>
    res.json().catch(() => ({})).then((body) => {
      if (!res.ok) throw new Error(body.message || body.error || `${res.status} ${res.statusText}`);
      return body;
    })
  );
}

const send = (path, body, method) =>
  api(path, {
    method: method || "POST",
    headers: { "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });

function escapeHtml(s) {
  return String(s ?? "").replace(/[&<>"']/g, (c) =>
    ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])
  );
}

function setStatus(text, isError) {
  $("status").textContent = text || "";
  $("status").className = isError ? "error" : "muted";
}

// --- state -------------------------------------------------------------------------
let hardware = [];
let projects = [];

function load() {
  const query = new URLSearchParams();
  if ($("search").value.trim()) query.set("q", $("search").value.trim());
  if ($("filter-status").value) query.set("status", $("filter-status").value);
  if ($("filter-project").value) query.set("project", $("filter-project").value);

  return api(`/hardware?${query}`)
    .then((data) => {
      hardware = data.hardware || [];
      render();
    })
    .catch((err) => {
      $("list").innerHTML = `<p class="error">Could not load: ${escapeHtml(err.message)}</p>`;
    });
}

function loadProjects() {
  return api("/projects")
    .then((data) => {
      projects = data.projects || [];
      const options =
        '<option value="">— none —</option>' +
        projects
          .map((p) => `<option value="${escapeHtml(p.slug)}">${escapeHtml(p.name)}</option>`)
          .join("");
      $("f-project").innerHTML = options;
      $("filter-project").innerHTML =
        '<option value="">Any project</option>' +
        projects
          .map((p) => `<option value="${escapeHtml(p.slug)}">${escapeHtml(p.name)}</option>`)
          .join("");
    })
    .catch(() => {
      // A workshop with no projects yet is the normal first-run state, not an error.
    });
}

function render() {
  const counts = hardware.reduce((acc, h) => {
    acc[h.status] = (acc[h.status] || 0) + 1;
    return acc;
  }, {});
  $("counts").textContent = hardware.length
    ? Object.keys(STATUS_LABELS)
        .filter((s) => counts[s])
        .map((s) => `${counts[s]} ${STATUS_LABELS[s].toLowerCase()}`)
        .join(" · ")
    : "";

  if (!hardware.length) {
    $("list").innerHTML =
      '<p class="muted">Nothing here yet. "Add hardware" records what you own and — the part that ' +
      'actually saves time — where you put it.</p>';
    return;
  }

  $("list").innerHTML = hardware.map(renderRow).join("");
  wireRows();
}

function renderRow(item) {
  const projectOptions =
    '<option value="">— none —</option>' +
    projects
      .map(
        (p) =>
          `<option value="${escapeHtml(p.slug)}"${p.slug === item.project_slug ? " selected" : ""}>${escapeHtml(
            p.name
          )}</option>`
      )
      .join("");
  const statusOptions = Object.keys(STATUS_LABELS)
    .map(
      (s) =>
        `<option value="${s}"${s === item.status ? " selected" : ""}>${escapeHtml(STATUS_LABELS[s])}</option>`
    )
    .join("");

  return `<article class="item status-${escapeHtml(item.status)}" data-id="${item.id}">
    <div class="item-head">
      <input class="designation" type="text" value="${escapeHtml(item.designation)}" aria-label="Designation">
      <input class="kind" type="text" value="${escapeHtml(item.kind)}" placeholder="kind" aria-label="Kind">
      <input class="quantity" type="number" min="0" step="1" value="${escapeHtml(item.quantity)}" aria-label="Quantity">
    </div>
    <div class="item-body">
      <label class="field">Where
        <input class="location" type="text" value="${escapeHtml(item.storage_location)}" placeholder="drawer 3, blue box">
      </label>
      <label class="field">Status
        <select class="status">${statusOptions}</select>
      </label>
      <label class="field">Project
        <select class="project">${projectOptions}</select>
      </label>
    </div>
    <p class="hint">${escapeHtml(STATUS_HINTS[item.status] || "")}</p>
    <div class="item-actions">
      <button class="save primary">Save</button>
      <button class="delete danger">Delete</button>
      <span class="row-status muted"></span>
    </div>
  </article>`;
}

function wireRows() {
  $("list").querySelectorAll(".item").forEach((el) => {
    const id = Number(el.dataset.id);
    const note = el.querySelector(".row-status");

    // Changing the project is the edit people come here to make, so it saves on the
    // spot rather than waiting for a Save nobody remembers to press. Everything else
    // is free text mid-typing and waits.
    el.querySelector(".project").addEventListener("change", () => saveRow(el, id, note));
    el.querySelector(".status").addEventListener("change", () => saveRow(el, id, note));
    el.querySelector(".save").addEventListener("click", () => saveRow(el, id, note));
    el.querySelector(".delete").addEventListener("click", () => {
      const name = el.querySelector(".designation").value.trim();
      if (!window.confirm(`Delete ${name}? This removes the record, not the thing.`)) return;
      send(`/hardware/${id}`, undefined, "DELETE")
        .then(() => load())
        .catch((err) => {
          note.textContent = err.message;
          note.className = "row-status error";
        });
    });
  });
}

function saveRow(el, id, note) {
  const body = {
    designation: el.querySelector(".designation").value.trim(),
    kind: el.querySelector(".kind").value.trim(),
    quantity: Number(el.querySelector(".quantity").value) || 0,
    storage_location: el.querySelector(".location").value.trim(),
    // Both are sent together so the server's own coupling rule decides the outcome —
    // including the one that matters, that naming a project on something already
    // `in_use` does not quietly demote it to merely reserved.
    status: el.querySelector(".status").value,
    project_slug: el.querySelector(".project").value || null,
  };
  note.textContent = "Saving…";
  note.className = "row-status muted";
  send(`/hardware/${id}`, body)
    .then(() => load())
    .catch((err) => {
      note.textContent = err.message;
      note.className = "row-status error";
    });
}

// --- add ---------------------------------------------------------------------------
$("add-btn").addEventListener("click", () => {
  $("add-form").hidden = !$("add-form").hidden;
  if (!$("add-form").hidden) $("f-designation").focus();
});
$("add-cancel").addEventListener("click", () => {
  $("add-form").hidden = true;
});

$("add-form").addEventListener("submit", (event) => {
  event.preventDefault();
  setStatus("Adding…");
  send("/hardware", {
    designation: $("f-designation").value.trim(),
    kind: $("f-kind").value.trim(),
    quantity: Number($("f-quantity").value) || 1,
    storage_location: $("f-location").value.trim(),
    status: $("f-status").value,
    project_slug: $("f-project").value || null,
    notes: $("f-notes").value.trim(),
  })
    .then(() => {
      $("add-form").reset();
      $("add-form").hidden = true;
      setStatus("");
      return load();
    })
    .catch((err) => setStatus(err.message, true));
});

// --- filters -----------------------------------------------------------------------
let searchTimer = null;
$("search").addEventListener("input", () => {
  clearTimeout(searchTimer);
  searchTimer = setTimeout(load, 200);
});
$("filter-status").addEventListener("change", load);
$("filter-project").addEventListener("change", load);

// --- Tabs ---------------------------------------------------------------------------
document.querySelectorAll(".tab").forEach((tab) => {
  tab.addEventListener("click", () => {
    document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t === tab));
    document.querySelectorAll(".panel").forEach((p) => p.classList.toggle("active", p.id === tab.dataset.tab));
    if (tab.dataset.tab === "cameras") loadCameras();
    if (tab.dataset.tab === "fleet") loadFleet();
  });
});

// --- Cameras --------------------------------------------------------------------------
// go2rtc's own stream page, in an iframe, one per camera. Deliberately NOT a hand-rolled
// WebRTC client: go2rtc already ships a player that negotiates WebRTC and falls back to
// MSE on its own, and reimplementing that here would be a second thing to keep working
// against a moving browser target. The workshop service never proxies the video — a
// Python HTTP server in the path of an H.264 stream is how a working camera starts
// stuttering.
function loadCameras() {
  const el = $("camera-grid");
  api("/cameras")
    .then((data) => {
      const cameras = data.cameras || [];
      const base = (data.go2rtc_url || "").replace(/\/$/, "");
      if (!cameras.length || !base) {
        el.innerHTML =
          '<p class="muted">No cameras configured — set WORKSHOP_CAMERAS and GO2RTC_URL in ' +
          "workshop.env. Both are needed: the names live here, the streams live in go2rtc.</p>";
        return;
      }
      el.innerHTML = cameras
        .map(
          (cam) => `<figure class="camera">
            <iframe src="${escapeHtml(base)}/stream.html?src=${encodeURIComponent(cam.stream)}&mode=webrtc"
                    loading="lazy" allowfullscreen title="${escapeHtml(cam.name)}"></iframe>
            <figcaption>${escapeHtml(cam.name)}</figcaption>
          </figure>`
        )
        .join("");
    })
    .catch((err) => {
      el.innerHTML = `<p class="error">Could not load cameras: ${escapeHtml(err.message)}</p>`;
    });
}

// --- Fleet scripts --------------------------------------------------------------------
// The admin surface for getting every machine into CheckMK. Two deliberate properties
// show up in this UI rather than being buried: an empty slot is LISTED (the thing a
// person setting this up needs to see is which platforms are still uncovered), and
// publish is a separate button from upload.
function loadFleet() {
  api("/fleet")
    .then((data) => {
      $("fleet-slots").innerHTML = (data.slots || []).map(renderSlot).join("");
      wireFleet();

      const reports = data.reports || [];
      $("fleet-reports").innerHTML = reports.length
        ? reports
            .map(
              (r) => `<div class="item ${r.ok ? (r.current ? "status-available" : "status-assigned") : "status-retired"}">
                <div class="item-head">
                  <strong>${escapeHtml(r.hostname)}</strong>
                  <span class="muted">${escapeHtml(r.platform)} · v${escapeHtml(r.version ?? "?")}</span>
                  <span class="muted">${r.ok ? "ok" : "FAILED"}${
                    r.ok && !r.current ? " · behind the published version" : ""
                  }</span>
                </div>
                <p class="hint">${escapeHtml(relativeTimeish(r.reported_at))}${
                  r.detail ? ` — ${escapeHtml(String(r.detail).slice(-200))}` : ""
                }</p>
              </div>`
            )
            .join("")
        : '<p class="muted">Nothing has reported yet. An endpoint appears here the first ' +
          "time its timer runs, which is also how you find out the timer is working.</p>";
    })
    .catch((err) => {
      $("fleet-slots").innerHTML = `<p class="error">Could not load: ${escapeHtml(err.message)}</p>`;
    });
}

function renderSlot(slot) {
  const live = slot.published_version;
  const versions = (slot.versions || [])
    .map(
      (v) => `<li>
        v${v.version} · <code>${escapeHtml(String(v.sha256).slice(0, 12))}</code>
        ${v.published ? '<strong>· published</strong>' : `<button class="row-btn publish-btn"
          data-platform="${escapeHtml(slot.platform)}" data-version="${v.version}">Publish</button>`}
        ${v.note ? `<span class="muted"> — ${escapeHtml(v.note)}</span>` : ""}
      </li>`
    )
    .join("");

  return `<article class="item ${live ? "status-available" : "status-retired"}">
    <div class="item-head">
      <strong>${escapeHtml(slot.label)}</strong>
      <span class="muted">${escapeHtml(slot.covers)}</span>
    </div>
    <p class="hint">${
      live
        ? `Published: v${live} · <code>${escapeHtml(String(slot.published_sha256).slice(0, 12))}</code>`
        : "<strong>Nothing published for this platform yet.</strong>"
    }</p>
    ${
      slot.runs_on_device
        ? ""
        : '<p class="hint"><strong>These devices cannot run a script.</strong> This slot holds the ' +
          "CheckMK-<em>server</em> side instead — an SNMP config or a special agent that polls them. " +
          "Nothing fetches it, so no endpoint will ever report against it; the slot exists so " +
          '"are these monitored?" has a visible answer.</p>'
    }
    <label class="field">Paste the script
      <textarea class="script-body" rows="6" data-platform="${escapeHtml(slot.platform)}"
        placeholder="#!/bin/sh&#10;# CheckMK agent install for ${escapeHtml(slot.label)}"></textarea>
    </label>
    <div class="item-actions">
      <input class="script-note" type="text" placeholder="what changed (optional)">
      <button class="row-btn upload-btn" data-platform="${escapeHtml(slot.platform)}">Upload as draft</button>
      <span class="row-status muted"></span>
    </div>
    ${versions ? `<ul class="versions">${versions}</ul>` : ""}
  </article>`;
}

function wireFleet() {
  $("fleet-slots").querySelectorAll(".upload-btn").forEach((btn) =>
    btn.addEventListener("click", () => {
      const card = btn.closest(".item");
      const note = card.querySelector(".row-status");
      const body = card.querySelector(".script-body").value;
      if (!body.trim()) {
        note.textContent = "Nothing to upload.";
        return;
      }
      note.textContent = "Uploading…";
      send("/fleet/upload", {
        platform: btn.dataset.platform,
        body,
        note: card.querySelector(".script-note").value.trim(),
      })
        .then((r) => {
          note.textContent = r.message || "Stored as a draft.";
          return loadFleet();
        })
        .catch((err) => {
          note.textContent = err.message;
          note.className = "row-status error";
        });
    })
  );

  $("fleet-slots").querySelectorAll(".publish-btn").forEach((btn) =>
    btn.addEventListener("click", () => {
      if (
        !window.confirm(
          `Publish v${btn.dataset.version} for ${btn.dataset.platform}?\n\n` +
            "Every endpoint of this platform will run it as root the next time its timer fires."
        )
      )
        return;
      send("/fleet/publish", {
        platform: btn.dataset.platform,
        version: Number(btn.dataset.version),
      })
        .then(() => loadFleet())
        .catch((err) => window.alert(err.message));
    })
  );
}

// --- System health --------------------------------------------------------------------
// One pill in the header rather than a page you have to remember to open: health you
// only see when you go looking for it is health you find out about from the failure.
const HEALTH_LABELS = { ok: "All OK", problem: "Problems", unreachable: "Unreachable" };

function loadHealth() {
  return api("/health")
    .then((data) => {
      const pill = $("health-pill");
      if (!data.configured) {
        // Nothing being watched and everything being fine look identical from here, and
        // only one of them is good news — so this says which.
        pill.textContent = "health: not configured";
        pill.className = "pill unknown";
        $("health-body").innerHTML =
          '<p class="muted">No CheckMK server and no firewalls configured. Nothing is being ' +
          "watched, which is not the same as nothing being wrong.</p>";
        return;
      }
      const overall = data.overall || "ok";
      pill.textContent = HEALTH_LABELS[overall] || overall;
      pill.className = `pill ${overall}`;
      $("health-body").innerHTML = (data.results || [])
        .map((row) => {
          const since = row.since ? ` — since ${escapeHtml(relativeTimeish(row.since))}` : "";
          const problems = (row.detail || [])
            .map((p) => `<li>${escapeHtml([p.host, p.service, p.state, p.output].filter(Boolean).join(" · "))}</li>`)
            .join("");
          return `<article class="health-row ${escapeHtml(row.state)}">
            <h3>${escapeHtml(row.target)} <span class="muted">(${escapeHtml(row.source)})</span></h3>
            <p>${escapeHtml(row.state)}${since}</p>
            <p class="muted">${escapeHtml(row.summary)}</p>
            ${problems ? `<ul>${problems}</ul>` : ""}
          </article>`;
        })
        .join("") || '<p class="muted">No samples yet — the poller runs every few minutes.</p>';
    })
    .catch(() => {
      $("health-pill").textContent = "health: unknown";
      $("health-pill").className = "pill unknown";
    });
}

function relativeTimeish(iso) {
  const then = Date.parse(iso || "");
  if (!Number.isFinite(then)) return iso || "an unknown time";
  const mins = Math.round((Date.now() - then) / 60000);
  if (mins < 60) return `${mins} min ago`;
  const hours = Math.round(mins / 60);
  return hours < 48 ? `${hours}h ago` : `${Math.round(hours / 24)}d ago`;
}

$("health-pill").addEventListener("click", () => {
  loadHealth();
  $("health-dialog").showModal();
});

loadHealth();
setInterval(loadHealth, 60000);
loadProjects().then(load);
