// files-overlay/core.js — Phase A scaffold (step 1/12).
//
// First module of the multi-file rewrite of files-overlay.js. Provides:
//   * Manager-element detection (.files-manager guard, same as legacy).
//   * Helpers (joinPath, parentOf, basename, escapeHtml, humanSize,
//     fetchJson, postJson, postForm, refreshFolder, findRowByPath,
//     cssEscape, scheduleRefresh) — duplicated from the legacy file
//     for the duration of Phase A. They de-duplicate in steps 2–4 as
//     features migrate out and the legacy file shrinks.
//   * window.__filesOverlay registry: feature modules call
//     registerHandler(op, fn); a document-level click listener here
//     dispatches matching actions via handleAction.
//
// Until Steps 2–4 populate the registry, the click listener here is a
// no-op — the legacy files-overlay.js still owns dispatch via its own
// switch-case at the same selector. Both listeners fire on every click
// during the transition; the new one matches and dispatches into an
// empty registry, the old one runs the actual handler. Per-op
// migration removes each case from the legacy switch as the
// corresponding feature module registers its handler.

(function () {
  "use strict";

  const manager = document.querySelector(".files-manager");
  if (!manager) return;

  const overlayId = manager.dataset.overlayId;
  const baseUrl = manager.dataset.baseUrl; // /overlays/<id>
  const treeRoot = manager.querySelector(".files-tree-root");
  const csrfToken =
    document.querySelector("meta[name='csrf-token']")?.getAttribute("content") || "";

  // ---------- helpers ----------

  function joinPath(folder, leaf) {
    folder = (folder || "").replace(/^\/+|\/+$/g, "");
    leaf = (leaf || "").replace(/^\/+|\/+$/g, "");
    if (folder && leaf) return folder + "/" + leaf;
    return folder || leaf;
  }

  function parentOf(rel) {
    const i = (rel || "").lastIndexOf("/");
    return i < 0 ? "" : rel.slice(0, i);
  }

  function basename(rel) {
    const i = (rel || "").lastIndexOf("/");
    return i < 0 ? rel : rel.slice(i + 1);
  }

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

  function humanSize(bytes) {
    if (bytes === undefined || bytes === null) return "";
    if (bytes < 1024) return bytes + " B";
    const units = ["KB", "MB", "GB", "TB"];
    let v = bytes / 1024;
    let u = "KB";
    for (const next of units) {
      u = next;
      if (v < 1024) break;
      v /= 1024;
    }
    return v.toFixed(1) + " " + u;
  }

  async function fetchJson(url, options) {
    options = options || {};
    options.headers = Object.assign({ Accept: "application/json" }, options.headers || {});
    if (options.method && options.method !== "GET") {
      options.headers["X-CSRF-Token"] = csrfToken;
    }
    options.credentials = "same-origin";
    const response = await fetch(url, options);
    let body = null;
    let rawText = "";
    try {
      rawText = await response.text();
      body = rawText ? JSON.parse(rawText) : null;
    } catch (_e) {
      body = null;
    }
    return { ok: response.ok, status: response.status, body, rawText };
  }

  async function postJson(url, payload) {
    return fetchJson(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  }

  async function postForm(url, formData) {
    return fetchJson(url, { method: "POST", body: formData });
  }

  // ---------- tree refresh ----------

  // Re-fetch a folder's listing partial and swap it into the tree.
  // `path === ""` refreshes the overlay root container.
  async function refreshFolder(path) {
    if (!treeRoot) return;
    const url = `${baseUrl}/files?path=${encodeURIComponent(path || "")}`;
    let html;
    try {
      const r = await fetch(url, {
        headers: { Accept: "text/html" },
        credentials: "same-origin",
      });
      if (!r.ok) {
        if (r.status === 404) {
          // Folder no longer exists — refresh its parent instead.
          if (path) await refreshFolder(parentOf(path));
          return;
        }
        return;
      }
      html = await r.text();
    } catch (_e) {
      return;
    }

    if (!path) {
      // Overlay root — swap into the synthetic root row's children div.
      const target = manager.querySelector(".files-root-children");
      if (!target) return;
      const empty = target.querySelector(".files-empty");
      if (empty) empty.remove();
      const existingUl = target.querySelector(":scope > ul.file-tree");
      if (existingUl) existingUl.remove();
      target.insertAdjacentHTML("beforeend", html);
      const newUl = target.querySelector(":scope > ul.file-tree");
      if (newUl && newUl.children.length === 0) {
        newUl.remove();
        const p = document.createElement("p");
        p.className = "muted files-empty";
        p.textContent = 'Empty — drop files here, or click "+ new file" on this row.';
        target.appendChild(p);
      }
      return;
    }

    // Sub-folder: find the matching folder row and swap its children div.
    const row = findRowByPath(path, "dir");
    if (!row) return;
    const childrenDiv = row.querySelector(":scope > .file-tree-children");
    const toggleBtn = row.querySelector(":scope > .file-tree-toggle");
    if (!childrenDiv || !toggleBtn) return;
    childrenDiv.innerHTML = html;
    childrenDiv.hidden = false;
    toggleBtn.setAttribute("aria-expanded", "true");
    toggleBtn.dataset.loaded = "1";
  }

  function findRowByPath(path, kind) {
    const sel = kind
      ? `[data-target-path="${cssEscape(path)}"][data-row-kind="${kind}"]`
      : `[data-target-path="${cssEscape(path)}"]`;
    return treeRoot ? treeRoot.querySelector(sel) : null;
  }

  function cssEscape(s) {
    if (window.CSS && window.CSS.escape) return window.CSS.escape(s);
    return String(s).replace(/["\\]/g, "\\$&");
  }

  // Debounce per-folder refreshes so a flurry of finishes coalesces.
  const pendingRefresh = new Map();
  function scheduleRefresh(path) {
    const key = path || "";
    if (pendingRefresh.has(key)) return;
    const timer = setTimeout(() => {
      pendingRefresh.delete(key);
      refreshFolder(key);
    }, 50);
    pendingRefresh.set(key, timer);
  }

  // ---------- action-dispatch registry ----------

  const handlers = new Map();

  function registerHandler(op, fn) {
    handlers.set(op, fn);
  }

  function handleAction(op, path, actionEl) {
    const fn = handlers.get(op);
    if (typeof fn === "function") fn(path, actionEl);
  }

  // ---------- action click delegation ----------
  //
  // Document-level delegation mirroring the legacy switch-case in
  // files-overlay.js. While the registry is empty (Step 1 state), this
  // is a no-op — the legacy file still owns dispatch. Steps 2–4
  // migrate one op at a time: feature module registers its handler
  // here, then the matching case is deleted from the legacy file.

  document.addEventListener("click", (event) => {
    const action = event.target?.closest?.(
      ".files-row-action[data-action], .file-tree-name-button[data-action]"
    );
    if (!action) return;
    if (!manager.contains(action)) return;
    const op = action.dataset.action;
    const path = action.dataset.targetPath || "";
    handleAction(op, path, action);
  });

  // ---------- public ----------

  window.__filesOverlay = {
    manager,
    overlayId,
    baseUrl,
    treeRoot,
    csrfToken,
    helpers: {
      joinPath,
      parentOf,
      basename,
      escapeHtml,
      humanSize,
      fetchJson,
      postJson,
      postForm,
      refreshFolder,
      findRowByPath,
      cssEscape,
      scheduleRefresh,
    },
    registerHandler,
    handleAction,
  };
})();
