// files-overlay/uploads.js — Phase A, Step 4.
//
// Owns the upload queue (concurrency 3 with XHR-based progress events)
// and the drag-drop UX on the file tree. Both OS-file drops (external)
// and row-to-folder moves (internal) flow through the same drop
// handler on treeRoot.
//
// Drag-drop is deliberately direct-bound to treeRoot (5 events:
// dragstart, dragend, dragover, dragleave, drop) per the plan escape
// hatch. They share coordinated highlight state across events, and
// delegation would obscure that coordination. treeRoot is persistent
// — never swapped out — so direct binding is safe.
//
// Upload-row cancel buttons converted from direct-bound per row to a
// single document-level delegated listener that finds the upload via
// data-upload-id on the row. uploads Map keyed by that id.
//
// Dispatch:
//   * "zip" registered into __filesOverlay → folder-zip download
//     (pure URL navigation). This is the last remaining action — with
//     Step 4 the legacy click-delegation switch goes away.
//
// withCollisionSuffix moves here from files-overlay.js (its biggest
// caller is the upload-conflict path). Exposed on __filesOverlay for
// editor.js's save-409 "keep both" branch.

(function () {
  "use strict";

  const fo = window.__filesOverlay;
  if (!fo) return;

  const { baseUrl, csrfToken, manager, treeRoot } = fo;
  const { joinPath, parentOf, basename, postJson, scheduleRefresh } = fo.helpers;

  const uploadsPanel = document.querySelector(".files-uploads");
  const uploadsList = uploadsPanel?.querySelector(".files-uploads-list");
  const uploadsClearBtn = uploadsPanel?.querySelector(".files-uploads-clear");

  // Attach a path-collision suffix: foo.txt → foo (1).txt.
  // Recognizes common compressed-tar double-extensions so
  // foo.tar.gz → foo (1).tar.gz, not foo.tar (1).gz.
  const DOUBLE_EXTS = [
    ".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst", ".tar.lz", ".tar.lzma",
  ];
  function withCollisionSuffix(path) {
    const slash = path.lastIndexOf("/");
    const stemStart = slash + 1;
    const basename = path.slice(stemStart);
    const lower = basename.toLowerCase();
    for (const ext of DOUBLE_EXTS) {
      if (lower.endsWith(ext) && basename.length > ext.length) {
        const cut = stemStart + basename.length - ext.length;
        return path.slice(0, cut) + " (1)" + path.slice(cut);
      }
    }
    // Single extension (or none). A dot must appear after the last
    // slash to count as an extension — preserves the legacy behavior
    // where a leading-dot basename like ".hidden" gets the suffix at
    // the end rather than at position 0.
    const dot = path.lastIndexOf(".");
    if (dot > slash && dot > -1) {
      return path.slice(0, dot) + " (1)" + path.slice(dot);
    }
    return path + " (1)";
  }
  fo.withCollisionSuffix = withCollisionSuffix;

  // ---------- upload queue + progress panel ----------

  const uploadQueue = [];
  let uploadActive = 0;
  const UPLOAD_CONCURRENCY = 3;

  // Map<uploadId, item> — used by the delegated cancel-button click
  // handler to find the item from the row's data-upload-id attribute.
  const uploads = new Map();
  let nextUploadId = 0;

  function showPanel() {
    if (uploadsPanel) uploadsPanel.hidden = false;
  }

  function maybeHidePanel() {
    if (!uploadsList) return;
    const anyActive = uploadsList.querySelector("[data-state='active'], [data-state='queued']");
    if (!anyActive && uploadsList.children.length === 0) {
      uploadsPanel.hidden = true;
      if (uploadsClearBtn) uploadsClearBtn.hidden = true;
    }
    const anyDone = uploadsList.querySelector("[data-state='done'], [data-state='error']");
    if (uploadsClearBtn) uploadsClearBtn.hidden = !anyDone;
  }

  function buildUploadRow(item) {
    const li = document.createElement("li");
    li.className = "files-uploads-row";
    li.dataset.state = "queued";
    li.dataset.uploadId = item.id;
    li.innerHTML = `
      <div class="files-uploads-row-meta">
        <span class="files-uploads-row-name"></span>
        <span class="files-uploads-row-target muted"></span>
      </div>
      <div class="files-uploads-row-progress"><div class="files-uploads-bar"></div></div>
      <div class="files-uploads-row-status">
        <span class="files-uploads-row-state">queued</span>
        <button type="button" class="link-button files-uploads-row-cancel" aria-label="Cancel">✕</button>
      </div>
    `;
    li.querySelector(".files-uploads-row-name").textContent = item.relative;
    li.querySelector(".files-uploads-row-target").textContent = `→ ${item.targetFolder || "(root)"}`;
    return li;
  }

  function setRowState(item, state, percent) {
    if (!item.row) return;
    item.row.dataset.state = state;
    const stateEl = item.row.querySelector(".files-uploads-row-state");
    const cancelBtn = item.row.querySelector(".files-uploads-row-cancel");
    const bar = item.row.querySelector(".files-uploads-bar");
    if (state === "queued") {
      stateEl.textContent = "queued";
      bar.style.width = "0%";
    } else if (state === "active") {
      stateEl.textContent = `${Math.round(percent || 0)}%`;
      bar.style.width = `${percent || 0}%`;
    } else if (state === "done") {
      stateEl.textContent = "done";
      bar.style.width = "100%";
      bar.classList.add("is-done");
      cancelBtn.textContent = "✓";
      cancelBtn.disabled = true;
    } else if (state === "cancelled") {
      stateEl.textContent = "cancelled";
      bar.classList.add("is-cancelled");
      cancelBtn.disabled = true;
    } else if (state === "error") {
      stateEl.textContent = item.errorText || "error";
      bar.classList.add("is-error");
      cancelBtn.textContent = "✕";
    } else if (state === "conflict") {
      stateEl.textContent = "conflict — overwrite / keep both";
    }
  }

  function cancelUpload(item) {
    if (item.xhr && item.row.dataset.state === "active") {
      item.cancelled = true;
      item.xhr.abort();
    }
    if (item.row.dataset.state === "queued") {
      const idx = uploadQueue.indexOf(item);
      if (idx >= 0) uploadQueue.splice(idx, 1);
      setRowState(item, "cancelled");
    }
  }

  // Delegated click handler for upload-row cancel buttons. Replaces
  // the per-row direct-bound listener that the legacy code attached
  // inside buildUploadRow. Reads the row's data-upload-id and looks up
  // the upload in the Map — works for rows added at any later time.
  document.addEventListener("click", (event) => {
    const btn = event.target?.closest?.(".files-uploads-row-cancel");
    if (!btn) return;
    const row = btn.closest(".files-uploads-row");
    if (!row || !uploadsList || !uploadsList.contains(row)) return;
    const id = row.dataset.uploadId;
    if (!id) return;
    const item = uploads.get(id);
    if (item) cancelUpload(item);
  });

  uploadsClearBtn?.addEventListener("click", () => {
    uploadsList.querySelectorAll("[data-state='done'], [data-state='error'], [data-state='cancelled']").forEach((row) => {
      const id = row.dataset.uploadId;
      if (id) uploads.delete(id);
      row.remove();
    });
    maybeHidePanel();
  });

  function enqueueUpload(file, targetFolder, relativePath) {
    const id = String(nextUploadId++);
    const item = {
      id,
      file,
      targetFolder,
      relative: relativePath || file.name,
      cancelled: false,
      xhr: null,
      errorText: null,
    };
    item.row = buildUploadRow(item);
    uploads.set(id, item);
    uploadsList.prepend(item.row);
    uploadQueue.push(item);
    showPanel();
    pump();
    return item;
  }

  function pump() {
    while (uploadActive < UPLOAD_CONCURRENCY && uploadQueue.length) {
      const item = uploadQueue.shift();
      runUpload(item);
    }
  }

  function runUpload(item, overwriteFlag) {
    uploadActive += 1;
    const fd = new FormData();
    fd.append("target_path", item.targetFolder || "");
    fd.append("relative_path", item.relative);
    fd.append("csrf_token", csrfToken);
    if (overwriteFlag) fd.append("overwrite", "1");
    fd.append("file", item.file, item.file.name);

    const xhr = new XMLHttpRequest();
    item.xhr = xhr;
    xhr.open("POST", `${baseUrl}/files/upload`);
    xhr.setRequestHeader("X-CSRF-Token", csrfToken);
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const pct = (event.loaded / event.total) * 100;
        setRowState(item, "active", pct);
      }
    };
    setRowState(item, "active", 0);
    xhr.onload = () => {
      uploadActive -= 1;
      if (xhr.status >= 200 && xhr.status < 300) {
        setRowState(item, "done");
        scheduleRefresh(joinPath(item.targetFolder || "", parentOf(item.relative)));
      } else if (xhr.status === 409 && !overwriteFlag) {
        setRowState(item, "conflict");
        const collidingPath = joinPath(item.targetFolder || "", item.relative);
        fo.askConflict(collidingPath).then((action) => {
          if (action === "overwrite") {
            runUpload(item, true);
          } else if (action === "keep-both") {
            item.relative = withCollisionSuffix(item.relative);
            item.row.querySelector(".files-uploads-row-name").textContent = item.relative;
            runUpload(item, false);
          } else {
            setRowState(item, "cancelled");
          }
        });
      } else {
        item.errorText = `HTTP ${xhr.status}`;
        try {
          const text = xhr.responseText;
          if (text) item.errorText = `HTTP ${xhr.status}: ${text.slice(0, 80)}`;
        } catch (_e) {}
        setRowState(item, "error");
      }
      maybeHidePanel();
      pump();
    };
    xhr.onerror = () => {
      uploadActive -= 1;
      item.errorText = item.cancelled ? "cancelled" : "network error";
      setRowState(item, item.cancelled ? "cancelled" : "error");
      maybeHidePanel();
      pump();
    };
    xhr.onabort = () => {
      uploadActive -= 1;
      setRowState(item, "cancelled");
      maybeHidePanel();
      pump();
    };
    xhr.send(fd);
  }

  // ---------- drag-drop on tree rows ----------

  function rowFromEvent(event) {
    return event.target.closest("[data-row-kind]");
  }

  function isInternalDrag(event) {
    return Array.from(event.dataTransfer.types).includes("application/x-files-overlay");
  }

  function isExternalDrag(event) {
    const types = Array.from(event.dataTransfer.types);
    return types.includes("Files");
  }

  // Expose containers (rows AND the empty-state placeholder + tree-root)
  // as drop targets — both for OS files and internal moves.
  if (treeRoot) {
    treeRoot.addEventListener("dragstart", (event) => {
      const row = rowFromEvent(event);
      if (!row) return;
      const path = row.dataset.targetPath;
      if (!path) return; // overlay root row isn't draggable
      event.dataTransfer.setData("application/x-files-overlay", path);
      event.dataTransfer.setData("text/plain", path);
      event.dataTransfer.effectAllowed = "move";
      row.classList.add("is-drag-source");
    });
    treeRoot.addEventListener("dragend", () => {
      treeRoot.querySelectorAll(".is-drag-source").forEach((el) => el.classList.remove("is-drag-source"));
      treeRoot.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target"));
    });

    treeRoot.addEventListener("dragover", (event) => {
      // Only react to file or row drags.
      if (!isExternalDrag(event) && !isInternalDrag(event)) return;
      const row = rowFromEvent(event);
      // Find the closest folder row, or the tree-root container itself for
      // root-level drops.
      const target = row && row.dataset.rowKind === "dir" ? row : null;
      const fallbackRoot = !row || row.dataset.rowKind !== "dir" ? treeRoot : null;
      const dropEl = target || fallbackRoot;
      if (!dropEl) return;
      event.preventDefault();
      event.dataTransfer.dropEffect = isInternalDrag(event) ? "move" : "copy";
      // Highlight the dropEl exclusively.
      treeRoot.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target"));
      dropEl.classList.add("is-drop-target");
    });
    treeRoot.addEventListener("dragleave", (event) => {
      // Leaving the whole tree clears highlights.
      if (!treeRoot.contains(event.relatedTarget)) {
        treeRoot.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target"));
      }
    });

    treeRoot.addEventListener("drop", async (event) => {
      const internal = isInternalDrag(event);
      const external = isExternalDrag(event);
      if (!internal && !external) return;
      event.preventDefault();
      const row = rowFromEvent(event);
      const dropFolder =
        row && row.dataset.rowKind === "dir" ? row.dataset.targetPath || "" : "";

      treeRoot.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target"));

      if (internal) {
        const src = event.dataTransfer.getData("application/x-files-overlay");
        if (!src) return;
        const dst = joinPath(dropFolder, basename(src));
        if (src === dst) return;
        // Cycle guard: refuse moving a folder into itself or descendant.
        if (dropFolder === src || dropFolder.startsWith(src + "/")) return;
        const r = await postJson(`${baseUrl}/files/move`, { src, dst });
        if (r.ok) {
          scheduleRefresh(parentOf(src));
          if (parentOf(src) !== dropFolder) scheduleRefresh(dropFolder);
        } else if (r.status === 409) {
          const action = await fo.askConflict(dst);
          if (action === "overwrite") {
            const r2 = await postJson(`${baseUrl}/files/move`, { src, dst, overwrite: true });
            if (r2.ok) {
              scheduleRefresh(parentOf(src));
              scheduleRefresh(dropFolder);
            }
          }
        } else {
          alert((r.body && r.body.error) || `Move failed (HTTP ${r.status}).`);
        }
        return;
      }

      // External drop — collect entries via webkitGetAsEntry where it
      // returns an Entry (real OS drag with folder support), and fall back
      // to getAsFile() for any item whose entry is null (synthetic events,
      // browsers without the API, or items that have no folder structure).
      const items = event.dataTransfer.items;
      const files = [];
      const tasks = [];
      if (items && items.length) {
        for (let i = 0; i < items.length; i++) {
          const item = items[i];
          if (typeof item.webkitGetAsEntry === "function") {
            const entry = item.webkitGetAsEntry();
            if (entry) {
              tasks.push(walkEntry(entry, "", files));
              continue;
            }
          }
          // Fallback: treat as a flat file.
          if (typeof item.getAsFile === "function") {
            const f = item.getAsFile();
            if (f) files.push({ file: f, rel: f.name });
          }
        }
      } else if (event.dataTransfer.files) {
        for (let i = 0; i < event.dataTransfer.files.length; i++) {
          files.push({ file: event.dataTransfer.files[i], rel: event.dataTransfer.files[i].name });
        }
      }
      await Promise.all(tasks);
      for (const f of files) {
        enqueueUpload(f.file, dropFolder, f.rel);
      }
    });
  }

  function walkEntry(entry, prefix, out) {
    return new Promise((resolve) => {
      if (entry.isFile) {
        entry.file((f) => {
          out.push({ file: f, rel: prefix + entry.name });
          resolve();
        });
      } else if (entry.isDirectory) {
        const reader = entry.createReader();
        const children = [];
        const readBatch = () => {
          reader.readEntries((batch) => {
            if (!batch.length) {
              Promise.all(
                children.map((c) => walkEntry(c, prefix + entry.name + "/", out))
              ).then(() => resolve());
              return;
            }
            for (const c of batch) children.push(c);
            readBatch();
          });
        };
        readBatch();
      } else {
        resolve();
      }
    });
  }

  // ---------- register action-registry handlers ----------

  // The last remaining file-row action — pure URL navigation. Could
  // equally live in core.js; placed here since uploads.js is the
  // closest thematic home (folder-level action like + new file etc.).
  fo.registerHandler("zip", (path) => {
    window.location.href = `${baseUrl}/files/download_zip?path=${encodeURIComponent(path)}`;
  });
})();
