/* Eno.Health Console — collapsible right-side API inspector. */
(function () {
  const emptyArray = [];

  const NetworkLog = (function () {
    let entries = [];
    let subscribers = [];
    let seq = 0;

    function publish() {
      subscribers.forEach((fn) => fn(entries));
    }

    function emit(event) {
      const entry = {
        id: "req_" + String(++seq).padStart(4, "0"),
        ts: Date.now(),
        method: "GET",
        status: 0,
        durationMs: 0,
        kind: "http",
        path: "",
        request: {},
        response: null,
        ...event,
      };
      entries = [entry, ...entries].slice(0, 300);
      publish();
      return entry;
    }

    function subscribe(fn) {
      subscribers.push(fn);
      fn(entries);
      return () => {
        subscribers = subscribers.filter((item) => item !== fn);
      };
    }

    function getAll() {
      return entries;
    }

    function clear() {
      entries = [];
      publish();
    }

    return { emit, subscribe, getAll, clear };
  })();

  window.EnoNet = NetworkLog;
  window.enoLog = (event) => NetworkLog.emit(event || {});
  let lastKafkaStreamErrorAt = 0;

  function parseMaybeJSON(value) {
    if (!value || typeof value !== "string") return value || null;
    try {
      return JSON.parse(value);
    } catch {
      return value;
    }
  }

  function requestPath(input) {
    const raw = typeof input === "string" ? input : input && input.url ? input.url : "";
    try {
      const url = new URL(raw, window.location.href);
      return url.origin === window.location.origin ? url.pathname + url.search : url.href;
    } catch {
      return raw || "unknown";
    }
  }

  function requestUrl(input) {
    const raw = typeof input === "string" ? input : input && input.url ? input.url : "";
    try {
      return new URL(raw, window.location.href).href;
    } catch {
      return raw || "";
    }
  }

  function requestMethod(input, init) {
    return String((init && init.method) || (input && input.method) || "GET").toUpperCase();
  }

  function compactHeaders(headers) {
    if (!headers) return {};
    if (typeof Headers !== "undefined" && headers instanceof Headers) return Object.fromEntries(headers.entries());
    if (Array.isArray(headers)) return Object.fromEntries(headers);
    return { ...headers };
  }

  function compactBody(body) {
    if (!body) return {};
    if (typeof FormData !== "undefined" && body instanceof FormData) {
      const out = {};
      body.forEach((value, key) => {
        out[key] = value && value.name ? { file: value.name, type: value.type || "", size: value.size || 0 } : value;
      });
      return out;
    }
    if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return Object.fromEntries(body.entries());
    if (typeof Blob !== "undefined" && body instanceof Blob) return { blob: { type: body.type || "", size: body.size || 0 } };
    return parseMaybeJSON(body) || {};
  }

  function installFetchLogger() {
    if (window.__enoFetchLoggerInstalled || typeof window.fetch !== "function") return;
    const nativeFetch = window.fetch.bind(window);
    window.__enoFetchLoggerInstalled = true;
    window.__enoNativeFetch = nativeFetch;
    // Patch fetch once so future screens are logged even if they bypass EnoAdminAPI.
    window.fetch = async function enoLoggedFetch(input, init) {
      const started = performance.now();
      const method = requestMethod(input, init);
      const path = requestPath(input);
      const url = requestUrl(input);
      const headers = compactHeaders((init && init.headers) || (input && input.headers));
      const request = compactBody(init && init.body);
      try {
        const response = await nativeFetch(input, init);
        let body = {};
        try {
          body = parseMaybeJSON(await response.clone().text()) || {};
        } catch (err) {
          body = { error: err.message || "Response body could not be read" };
        }
        NetworkLog.emit({
          method,
          path,
          url,
          status: response.status,
          durationMs: Math.round(performance.now() - started),
          headers,
          request,
          response: body,
        });
        return response;
      } catch (err) {
        NetworkLog.emit({
          method,
          path,
          url,
          status: 0,
          durationMs: Math.round(performance.now() - started),
          headers,
          request,
          response: { error: err.message || "Network request failed" },
        });
        throw err;
      }
    };
  }

  installFetchLogger();

  function debugStreamURL() {
    const api = window.EnoAdminAPI;
    if (api && api.adminKafkaDebugEventsURL) return api.adminKafkaDebugEventsURL();
    const base = api && api.config ? api.config().baseUrl : "/api";
    return String(base || "/api").replace(/\/+$/, "") + "/v1/admin/debug/kafka-events";
  }

  function jsonString(value) {
    try {
      return JSON.stringify(value, null, 2);
    } catch {
      return String(value);
    }
  }

  function copyText(value) {
    const text = String(value == null ? "" : value);
    if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
    const el = document.createElement("textarea");
    el.value = text;
    el.setAttribute("readonly", "readonly");
    el.style.position = "fixed";
    el.style.left = "-9999px";
    document.body.appendChild(el);
    el.select();
    document.execCommand("copy");
    document.body.removeChild(el);
    return Promise.resolve();
  }

  function shellQuote(value) {
    return "'" + String(value).replace(/'/g, "'\\''") + "'";
  }

  function hasBody(value) {
    if (value == null) return false;
    if (typeof value === "string") return value.length > 0;
    return typeof value === "object" ? Object.keys(value).length > 0 : true;
  }

  function curlForEntry(entry) {
    const lines = ["curl -X " + shellQuote(entry.method || "GET") + " " + shellQuote(entry.url || (entry.path && entry.path.startsWith("/") ? window.location.origin + entry.path : entry.path || ""))];
    Object.entries(entry.headers || {}).forEach(([key, value]) => {
      lines.push("  -H " + shellQuote(key + ": " + value));
    });
    if (hasBody(entry.request)) {
      lines.push("  --data-raw " + shellQuote(typeof entry.request === "string" ? entry.request : jsonString(entry.request)));
    }
    return lines.join(" \\\n");
  }

  function CopyIconButton({ title, icon = "copy", value, onCopied }) {
    const [done, setDone] = useState(false);
    const click = (event) => {
      event.stopPropagation();
      copyText(value).then(() => {
        setDone(true);
        onCopied && onCopied();
        setTimeout(() => setDone(false), 1200);
      }).catch(() => setDone(false));
    };
    return h("button", { className: "net-copy" + (done ? " copied" : ""), title, onClick: click },
      h(Icon, { name: done ? "check" : icon, size: 13 }));
  }

  function HighlightedJSON({ value }) {
    const source = jsonString(value);
    const tokens = [];
    const re = /("(\\.|[^"\\])*"\s*:)|("(\\.|[^"\\])*")|(\b(true|false|null)\b)|(-?\d+\.?\d*(e[+-]?\d+)?)/gi;
    let last = 0;
    let match;
    let idx = 0;
    while ((match = re.exec(source)) !== null) {
      if (match.index > last) tokens.push(h("span", { key: idx++ }, source.slice(last, match.index)));
      let cls = "j-num";
      if (match[1]) cls = "j-key";
      else if (match[3]) cls = "j-str";
      else if (match[5]) cls = "j-bool";
      tokens.push(h("span", { key: idx++, className: cls }, match[0]));
      last = match.index + match[0].length;
    }
    if (last < source.length) tokens.push(h("span", { key: idx++ }, source.slice(last)));
    return h("pre", { className: "dbg-json" }, tokens);
  }

  function statusClass(status) {
    const code = Number(status || 0);
    if (code >= 500) return "st-5xx";
    if (code >= 400) return "st-4xx";
    if (code >= 300) return "st-3xx";
    if (code >= 200) return "st-2xx";
    return "st-pending";
  }

  function methodClass(method) {
    return "mt-" + String(method || "get").toLowerCase();
  }

  function kafkaStatusClass(status) {
    const value = String(status || "").toLowerCase();
    if (value === "published" || value === "sent" || value === "delivered") return "kst-ok";
    if (value === "pending") return "kst-wait";
    if (value === "failed" || value === "dead_letter") return "kst-bad";
    return "kst-neutral";
  }

  function agoShort(ts) {
    const seconds = Math.round((Date.now() - ts) / 1000);
    if (seconds < 5) return "now";
    if (seconds < 60) return seconds + "s";
    if (seconds < 3600) return Math.floor(seconds / 60) + "m";
    return Math.floor(seconds / 3600) + "h";
  }

  function NetRow({ entry, open, onToggle }) {
    if (entry.kind === "kafka") return h(KafkaRow, { entry, open, onToggle });
    return h("div", { className: "net-row" + (open ? " open" : "") },
      h("button", { className: "net-row-head", onClick: onToggle },
        h("span", { className: "net-m " + methodClass(entry.method) }, entry.method),
        h("span", { className: "net-path", title: entry.path }, entry.path),
        h("span", { className: "net-st " + statusClass(entry.status) }, entry.status || "ERR"),
        h("span", { className: "net-age" }, agoShort(entry.ts))),
      open && h("div", { className: "net-row-body" },
        h("div", { className: "net-meta" },
          h("span", { className: "net-meta-line" },
            h("span", { className: "net-meta-k" }, "Endpoint "),
            h("button", { className: "net-endpoint mono", title: "Copy endpoint", onClick: (event) => { event.stopPropagation(); copyText(entry.path); } }, entry.method, " ", entry.path),
            h(CopyIconButton, { title: "Copy endpoint", value: entry.path }),
            h(CopyIconButton, { title: "Copy as curl", icon: "curl", value: curlForEntry(entry) })),
          h("span", null, h("span", { className: "net-meta-k" }, "Status "), h("span", { className: "mono " + statusClass(entry.status).replace("st-", "stx-") }, entry.status || "ERR"), " · ", entry.durationMs || 0, "ms")),
        h("div", { className: "net-block-head" },
          h("div", { className: "net-block-label" }, "Request"),
          h(CopyIconButton, { title: "Copy request JSON", value: jsonString(entry.request || {}) })),
        h(HighlightedJSON, { value: entry.request || {} }),
        h("div", { className: "net-block-head" },
          h("div", { className: "net-block-label" }, "Response"),
          h(CopyIconButton, { title: "Copy response JSON", value: jsonString(entry.response == null ? {} : entry.response) })),
        h(HighlightedJSON, { value: entry.response == null ? {} : entry.response })));
  }

  function KafkaRow({ entry, open, onToggle }) {
    const envelope = {
      topic: entry.topic,
      event_type: entry.eventType,
      event_id: entry.eventId,
      sequence_id: entry.sequenceId,
      status: entry.kafkaStatus,
      attempts: entry.attempts,
      key_hash: entry.keyHash,
      headers: entry.headers || {},
      payload: entry.payload || {},
      created_at: entry.createdAt,
      published_at: entry.publishedAt || null,
      redacted: true,
    };
    return h("div", { className: "net-row kafka-row" + (open ? " open" : "") },
      h("button", { className: "net-row-head", onClick: onToggle },
        h("span", { className: "net-m mt-kafka" }, "KAFKA"),
        h("span", { className: "net-path", title: entry.topic }, entry.topic || "Kafka event"),
        h("span", { className: "net-st net-kstatus " + kafkaStatusClass(entry.kafkaStatus) }, entry.kafkaStatus || "event"),
        h("span", { className: "net-age" }, agoShort(entry.ts))),
      open && h("div", { className: "net-row-body" },
        h("div", { className: "net-meta" },
          h("span", { className: "net-meta-line" },
            h("span", { className: "net-meta-k" }, "Topic "),
            h("button", { className: "net-endpoint mono", title: "Copy topic", onClick: (event) => { event.stopPropagation(); copyText(entry.topic || ""); } }, entry.topic || "unknown"),
            h(CopyIconButton, { title: "Copy topic", value: entry.topic || "" }),
            h(CopyIconButton, { title: "Copy Kafka envelope", value: jsonString(envelope) })),
          h("span", null, h("span", { className: "net-meta-k" }, "Event "), h("span", { className: "mono" }, entry.eventType || "unknown"), " · key hash ", h("span", { className: "mono" }, entry.keyHash || "n/a")),
          h("span", null, h("span", { className: "net-meta-k" }, "Status "), h("span", { className: "mono " + kafkaStatusClass(entry.kafkaStatus) }, entry.kafkaStatus || "event"), " · attempts ", entry.attempts || 0)),
        h("div", { className: "net-block-head" },
          h("div", { className: "net-block-label" }, "Headers"),
          h(CopyIconButton, { title: "Copy Kafka headers JSON", value: jsonString(entry.headers || {}) })),
        h(HighlightedJSON, { value: entry.headers || {} }),
        h("div", { className: "net-block-head" },
          h("div", { className: "net-block-label" }, "Payload summary"),
          h(CopyIconButton, { title: "Copy Kafka payload summary JSON", value: jsonString(entry.payload || {}) })),
        h(HighlightedJSON, { value: entry.payload || {} }),
        entry.redactedNote && h("div", { className: "net-redacted-note" }, entry.redactedNote)));
  }

  function DebugPanel() {
    const [open, setOpen] = useState(() => {
      try { return localStorage.getItem("eno_dbg") === "1"; } catch { return false; }
    });
    const [pinned, setPinned] = useState(() => {
      try { return localStorage.getItem("eno_dbg_pin") === "1"; } catch { return false; }
    });
    const [mode, setMode] = useState(() => {
      try { return localStorage.getItem("eno_dbg_mode") || "api"; } catch { return "api"; }
    });
    const [entries, setEntries] = useState(NetworkLog.getAll());
    const [expanded, setExpanded] = useState(null);
    const [, setTick] = useState(0);
    const apiEntries = entries.filter((entry) => entry.kind !== "kafka");
    const kafkaEntries = entries.filter((entry) => entry.kind === "kafka");
    const shownEntries = mode === "events" ? kafkaEntries : apiEntries;
    const emptyCopy = mode === "events"
      ? "No Kafka events yet. Trigger an ingestion flow or wait for the outbox stream."
      : "No API calls yet. Navigate the console to log request and response JSON.";

    useEffect(() => NetworkLog.subscribe(setEntries), []);
    useEffect(() => {
      try { localStorage.setItem("eno_dbg", open ? "1" : "0"); } catch {}
    }, [open]);
    useEffect(() => {
      try { localStorage.setItem("eno_dbg_pin", pinned ? "1" : "0"); } catch {}
    }, [pinned]);
    useEffect(() => {
      try { localStorage.setItem("eno_dbg_mode", mode); } catch {}
      setExpanded(null);
    }, [mode]);
    useEffect(() => {
      document.body.classList.toggle("dbg-pinned", pinned && open);
      return () => document.body.classList.remove("dbg-pinned");
    }, [pinned, open]);
    useEffect(() => {
      if (!open || pinned) return undefined;
      const closeOutside = (event) => {
        if (event.target.closest && (event.target.closest(".dbg-panel") || event.target.closest(".dbg-rail"))) return;
        setOpen(false);
      };
      document.addEventListener("pointerdown", closeOutside, true);
      return () => document.removeEventListener("pointerdown", closeOutside, true);
    }, [open, pinned]);
    useEffect(() => {
      const handler = (event) => {
        if (event.altKey && (event.key === "d" || event.key === "D" || event.key === "∂")) {
          event.preventDefault();
          setOpen((value) => !value);
        }
      };
      window.addEventListener("keydown", handler);
      return () => window.removeEventListener("keydown", handler);
    }, []);
    useEffect(() => {
      if (!open) return undefined;
      const timer = setInterval(() => setTick((value) => value + 1), 15000);
      return () => clearInterval(timer);
    }, [open]);
    useEffect(() => {
      if (typeof EventSource === "undefined") return undefined;
      const source = new EventSource(debugStreamURL());
      source.addEventListener("kafka.event", (event) => {
        let data = {};
        try {
          data = JSON.parse(event.data || "{}");
        } catch {
          data = { payload: { parse_error: "Kafka debug event could not be decoded" } };
        }
        NetworkLog.emit({
          kind: "kafka",
          method: "KAFKA",
          topic: data.topic || "",
          path: data.topic || "Kafka event",
          eventType: data.event_type || "",
          eventId: data.event_id || "",
          sequenceId: data.sequence_id || Number(event.lastEventId || 0),
          kafkaStatus: data.status || "",
          attempts: data.attempts || 0,
          keyHash: data.key_hash || "",
          headers: data.headers || {},
          payload: data.payload || {},
          createdAt: data.created_at || "",
          publishedAt: data.published_at || "",
          redactedNote: data.redacted_note || "",
          status: data.status || "event",
        });
      });
      source.addEventListener("stream.error", (event) => {
        let data = {};
        try { data = JSON.parse(event.data || "{}"); } catch {}
        NetworkLog.emit({
          kind: "kafka",
          method: "KAFKA",
          path: "Kafka debug stream",
          topic: "Kafka debug stream",
          kafkaStatus: "failed",
          status: "failed",
          payload: data,
          redactedNote: "The Kafka debug stream reported an error.",
        });
      });
      source.onerror = () => {
        const now = Date.now();
        if (now - lastKafkaStreamErrorAt < 10000) return;
        lastKafkaStreamErrorAt = now;
        NetworkLog.emit({
          kind: "kafka",
          method: "KAFKA",
          path: "Kafka debug stream",
          topic: "Kafka debug stream",
          kafkaStatus: "pending",
          status: "pending",
          payload: { state: "reconnecting" },
          redactedNote: "Kafka debug stream is reconnecting.",
        });
      };
      return () => source.close();
    }, []);

    return h(React.Fragment, null,
      h("button", { className: "dbg-rail" + (open ? " hidden" : ""), title: "Open API inspector (Alt+D)", onClick: () => setOpen(true) },
        h(Icon, { name: "code", size: 16 }),
        h("span", { className: "dbg-rail-label" }, "Network"),
        entries.length > 0 && h("span", { className: "dbg-rail-badge" }, entries.length)),
      h("aside", { className: "dbg-panel" + (open ? " open" : "") + (pinned ? " pinned" : "") },
        h("div", { className: "dbg-head" },
          h("div", { className: "dbg-head-l" },
            h(Icon, { name: "code", size: 16 }),
            h("span", { className: "dbg-title" }, "Debug"),
            h("span", { className: "dbg-badge" }, entries.length, " events")),
          h("div", { className: "dbg-head-r" },
            h("button", { className: "dbg-ibtn" + (pinned ? " on" : ""), title: pinned ? "Unpin panel" : "Pin panel", onClick: () => setPinned((value) => !value) }, h(Icon, { name: "pin", size: 15 })),
            h("button", { className: "dbg-ibtn", title: "Clear log", onClick: () => { NetworkLog.clear(); setExpanded(null); } }, h(Icon, { name: "trash", size: 15 })),
            h("button", { className: "dbg-ibtn", title: "Collapse", onClick: () => setOpen(false) }, h(Icon, { name: "chevronR", size: 17 })))),
        h("div", { className: "dbg-switch" },
          h("button", { className: "dbg-switch-btn" + (mode === "api" ? " on" : ""), onClick: () => setMode("api") },
            h(Icon, { name: "code", size: 13 }),
            h("span", null, "API"),
            h("span", { className: "dbg-switch-count" }, apiEntries.length)),
          h("button", { className: "dbg-switch-btn" + (mode === "events" ? " on" : ""), onClick: () => setMode("events") },
            h(Icon, { name: "activity", size: 13 }),
            h("span", null, "Events"),
            h("span", { className: "dbg-switch-count" }, kafkaEntries.length))),
        h("div", { className: "net-colhead" },
          h("span", null, "Kind"),
          h("span", { style: { flex: 1 } }, mode === "events" ? "Topic" : "Endpoint"),
          h("span", null, "Status"),
          h("span", null, "Age")),
        h("div", { className: "dbg-scroll" },
          (shownEntries || emptyArray).length === 0
            ? h("div", { className: "net-empty" }, emptyCopy)
            : shownEntries.map((entry) => h(NetRow, {
              key: entry.id,
              entry,
              open: expanded === entry.id,
              onToggle: () => setExpanded((value) => (value === entry.id ? null : entry.id)),
            })),
          h("div", { className: "dbg-foot" }, "Toggle with ", h("kbd", null, "Alt"), h("kbd", null, "D"), " · HTTP + redacted Kafka"))));
  }

  window.DebugPanel = DebugPanel;
})();
