// console-history.js
// Binds ArrowUp/Down history recall to [data-console-form] elements.
// Mirrors the style of sse.js: vanilla JS, dataset attributes, no framework.

function bindConsoleForm(form) {
  if (form.dataset.consoleHistoryBound === "true") {
    return;
  }
  form.dataset.consoleHistoryBound = "true";

  const serverId = form.dataset.serverId;
  const input = form.querySelector("input[name='command']");
  if (!input || !serverId) {
    return;
  }

  // --- History cache state ---
  // Entries are stored newest-first to match the API response shape.
  let cache = [];
  let cacheLoaded = false;
  let loadingPromise = null;
  let cursor = -1;       // -1 = "not in history" (at the live input)
  let snapshot = "";     // saved input value for restoring via ArrowDown
  let oldestId = null;   // id of the oldest cached entry, used for pagination
  let exhausted = false; // true when we've fetched all available history
  let pendingCommand = null; // captured before HTMX submit; used by afterRequest

  async function loadHistory(params) {
    const url = new URL(`/servers/${serverId}/console/history`, location.origin);
    url.searchParams.set("limit", "50");
    if (params && params.before != null) {
      url.searchParams.set("before", params.before);
    }
    try {
      const res = await fetch(url.toString());
      if (!res.ok) return [];
      return await res.json();
    } catch {
      return [];
    }
  }

  async function ensureLoaded() {
    if (cacheLoaded) return;
    if (!loadingPromise) {
      loadingPromise = (async () => {
        const entries = await loadHistory();
        cache = entries; // newest-first from API
        if (cache.length > 0) {
          oldestId = cache[cache.length - 1].id;
        }
        if (entries.length < 50) {
          exhausted = true;
        }
        cacheLoaded = true;
      })();
    }
    await loadingPromise;
  }

  async function fetchOlderPage() {
    if (exhausted || oldestId == null) return;
    const entries = await loadHistory({ before: oldestId });
    if (entries.length === 0) {
      exhausted = true;
      return;
    }
    cache = cache.concat(entries); // append older entries at the end
    oldestId = cache[cache.length - 1].id;
    if (entries.length < 50) {
      exhausted = true;
    }
  }

  input.addEventListener("focus", () => {
    ensureLoaded();
  }, { once: true });

  input.addEventListener("keydown", async (event) => {
    if (event.key !== "ArrowUp" && event.key !== "ArrowDown") {
      return;
    }
    event.preventDefault();

    // Ensure we have history before navigating.
    await ensureLoaded();

    if (event.key === "ArrowUp") {
      if (cursor === -1) {
        // Entering history from live input — save whatever is typed.
        snapshot = input.value;
      }

      const nextCursor = cursor + 1;

      // If we've reached the end of cache, try fetching an older page.
      if (nextCursor >= cache.length) {
        await fetchOlderPage();
      }

      if (nextCursor < cache.length) {
        cursor = nextCursor;
        input.value = cache[cursor].command;
      }
      // else: already at oldest end, stay put.

    } else {
      // ArrowDown
      if (cursor <= -1) {
        // Already at live input, nothing to do.
        return;
      }

      const nextCursor = cursor - 1;
      if (nextCursor < 0) {
        // Return to live input, restore snapshot.
        cursor = -1;
        input.value = snapshot;
      } else {
        cursor = nextCursor;
        input.value = cache[cursor].command;
      }
    }
  });

  // Capture the command value before HTMX submits the form; avoids relying on
  // event.detail.requestConfig.parameters which changed between HTMX 1.x / 2.x.
  form.addEventListener("htmx:beforeRequest", () => {
    const commandInput = form.querySelector("input[name='command']");
    pendingCommand = commandInput ? commandInput.value : null;
  });

  // After a successful HTMX POST: prepend the sent command to cache.
  form.addEventListener("htmx:afterRequest", (event) => {
    if (!event.detail.successful) return;
    // Use the value captured before the request; fall back to snapshot.
    const command = pendingCommand || snapshot || "";
    pendingCommand = null;
    if (!command) return;
    // Prepend as the newest entry.
    // id=null is safe here: pagination uses oldestId (the real persisted-row id)
    // for ?before= queries, so a synthetic null-id entry doesn't break paging.
    cache.unshift({ id: null, command });
    // Reset cursor so ArrowUp immediately recalls this command.
    cursor = -1;
    snapshot = "";
  });
}

function bindAllConsoleForms(root) {
  if (!root) return;
  const scope = root.matches && root.matches("[data-console-form]") ? [root] : [];
  if (root.querySelectorAll) {
    root.querySelectorAll("[data-console-form]").forEach((el) => scope.push(el));
  }
  scope.forEach(bindConsoleForm);
}

function scrollConsolesToBottom(root) {
  if (!root) return;
  const scope = root.matches && root.matches("[data-autoscroll]") ? [root] : [];
  if (root.querySelectorAll) {
    root.querySelectorAll("[data-autoscroll]").forEach((el) => scope.push(el));
  }
  scope.forEach((el) => {
    el.scrollTop = el.scrollHeight;
  });
}

document.addEventListener("DOMContentLoaded", () => {
  scrollConsolesToBottom(document);
  bindAllConsoleForms(document);
});

// Support HTMX-injected content (mirrors sse.js pattern).
document.addEventListener("htmx:load", (event) => {
  scrollConsolesToBottom(event.detail.elt);
  bindAllConsoleForms(event.detail.elt);
});
