// files-overlay/editor.js — Phase A, Step 2.
//
// Owns the editor flows during Phase A. Dual-purpose: drives both the
// legacy inline #files-editor-modal <dialog> (binary-replace + create-
// new-file) and the URL-addressable modal swapped into #modal-content
// (editable text files). Phase B migrates the legacy flows to URL-
// addressable too and removes the legacy branches here.
//
// Action-registry dispatch (registered into __filesOverlay):
//   * "new-file"  → openEditorTextNew(folder)  (legacy dialog)
//   * "edit"      → URL-addressable modal for editable files;
//                   openEditorForFile(path, false) for binary.
//
// Save / delete: a single document-level click listener handles both
// the legacy dialog and the URL-addressable modal, discriminated by
// which ancestor contains the click target.
//
// Direct-bound (per plan, escape hatch): filename `input`, content
// textarea `input` + `keydown` — high-frequency events on persistent
// inputs inside the persistent legacy dialog.
//
// Cross-module deps consumed via window.__filesOverlay (set by
// core.js): manager, overlayId, baseUrl, csrfToken, helpers, plus
// askConflict (set by the legacy files-overlay.js, used here for save
// 409-conflict handling).

(function () {
  "use strict";

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

  const { overlayId, baseUrl, csrfToken } = fo;
  const {
    joinPath, parentOf, basename, humanSize,
    fetchJson, postJson, postForm, scheduleRefresh,
  } = fo.helpers;

  // Legacy inline dialog — present in Phase A. Phase B deletes it; the
  // legacy-branch handlers below short-circuit on its absence.
  const editorDialog = document.getElementById("files-editor-modal");

  // ---------- legacy editor state + DOM refs ----------

  const editor = {
    mode: null, // "text" | "binary"
    creating: false,
    originalPath: null,
    folder: null,
    queuedReplacement: null, // File object
  };

  const editorEls = editorDialog ? {
    title: editorDialog.querySelector(".files-editor-title-text"),
    filename: editorDialog.querySelector(".files-editor-filename"),
    renameHint: editorDialog.querySelector(".files-editor-rename-hint"),
    renameFrom: editorDialog.querySelector(".files-rename-from"),
    renameTo: editorDialog.querySelector(".files-rename-to"),
    textPanel: editorDialog.querySelector(".files-editor-text"),
    contentBox: editorDialog.querySelector(".files-editor-content"),
    byteCount: editorDialog.querySelector(".files-editor-byte-count"),
    binaryPanel: editorDialog.querySelector(".files-editor-binary"),
    binarySize: editorDialog.querySelector(".files-editor-binary-size"),
    replaceZone: editorDialog.querySelector(".files-editor-replace-zone"),
    replaceIdle: editorDialog.querySelector(".files-editor-replace-idle"),
    replaceQueued: editorDialog.querySelector(".files-editor-replace-queued"),
    replaceName: editorDialog.querySelector(".files-editor-replace-name"),
    replaceSize: editorDialog.querySelector(".files-editor-replace-size"),
    replaceClear: editorDialog.querySelector(".files-editor-replace-clear"),
    replaceBrowse: editorDialog.querySelector(".files-editor-replace-browse"),
    replaceInput: editorDialog.querySelector(".files-editor-replace-input"),
    deleteBtn: editorDialog.querySelector(".files-editor-delete"),
    downloadBtn: editorDialog.querySelector(".files-editor-download"),
    saveBtn: editorDialog.querySelector(".files-editor-save"),
  } : null;

  // ---------- CM6 bridge + UI updates (legacy dialog) ----------

  // Bridge to the CodeMirror 6 controller, set up by static/js/editor.js
  // on the .files-editor-content textarea. Falls back to the textarea
  // directly if the bundle didn't load (no-JS fallback / file open
  // before the controller has been mounted).
  function getEditorValue() {
    return (window.__filesEditor && window.__filesEditor.getValue)
      ? window.__filesEditor.getValue()
      : editorEls.contentBox.value;
  }
  function setEditorValue(text) {
    if (window.__filesEditor && window.__filesEditor.setContent) {
      window.__filesEditor.setContent(text);
    } else {
      editorEls.contentBox.value = text;
    }
  }

  function setEditorTitle(text) {
    editorEls.title.textContent = text;
  }

  function updateByteCount() {
    const bytes = new TextEncoder().encode(getEditorValue()).length;
    editorEls.byteCount.textContent = `UTF-8 · ${bytes} bytes`;
  }

  function updateRenameHint() {
    const current = editorEls.filename.value.trim();
    const original = basename(editor.originalPath || "");
    if (editor.creating || !current || current === original) {
      editorEls.renameHint.hidden = true;
      return;
    }
    editorEls.renameFrom.textContent = original;
    editorEls.renameTo.textContent = current;
    editorEls.renameHint.hidden = false;
  }

  function updateSaveEnabled() {
    if (editor.mode === "binary" && !editor.creating) {
      const filenameChanged =
        editorEls.filename.value.trim() !== basename(editor.originalPath || "");
      const hasReplacement = !!editor.queuedReplacement;
      editorEls.saveBtn.disabled = !filenameChanged && !hasReplacement;
      editorEls.saveBtn.textContent = "Save";
    } else if (editor.creating) {
      editorEls.saveBtn.disabled = !editorEls.filename.value.trim();
      editorEls.saveBtn.textContent = "Create";
    } else {
      editorEls.saveBtn.disabled = false;
      editorEls.saveBtn.textContent = "Save";
    }
  }

  function setQueuedReplacement(file) {
    editor.queuedReplacement = file;
    if (file) {
      editorEls.replaceIdle.hidden = true;
      editorEls.replaceQueued.hidden = false;
      editorEls.replaceName.textContent = file.name;
      editorEls.replaceSize.textContent = humanSize(file.size);
    } else {
      editorEls.replaceIdle.hidden = false;
      editorEls.replaceQueued.hidden = true;
    }
    updateSaveEnabled();
  }

  // ---------- legacy editor openers ----------

  function openEditorTextNew(folder) {
    if (!editorDialog) return;
    editor.mode = "text";
    editor.creating = true;
    editor.originalPath = null;
    editor.folder = folder;
    editor.queuedReplacement = null;

    setEditorTitle(`${folder ? folder + "/" : ""}…new file`);
    editorEls.filename.value = "";
    editorEls.filename.disabled = false;
    setEditorValue("");
    editorEls.contentBox.disabled = false;
    editorEls.renameHint.hidden = true;
    editorEls.textPanel.hidden = false;
    editorEls.binaryPanel.hidden = true;
    editorEls.deleteBtn.hidden = true;
    editorEls.downloadBtn.hidden = true;
    editorEls.saveBtn.textContent = "Create";
    updateByteCount();
    updateSaveEnabled();
    editorDialog.showModal();
    setTimeout(() => editorEls.filename.focus(), 0);
  }

  async function openEditorForFile(path, isEditable) {
    if (!editorDialog) return;
    editor.creating = false;
    editor.originalPath = path;
    editor.folder = parentOf(path);
    editor.queuedReplacement = null;
    setQueuedReplacement(null);

    editorEls.filename.value = basename(path);
    editorEls.filename.disabled = false;
    editorEls.renameHint.hidden = true;
    editorEls.deleteBtn.hidden = false;
    editorEls.downloadBtn.hidden = false;
    editorEls.downloadBtn.href = `${baseUrl}/files/download?path=${encodeURIComponent(path)}`;
    setEditorTitle(path);

    if (isEditable) {
      editor.mode = "text";
      editorEls.textPanel.hidden = false;
      editorEls.binaryPanel.hidden = true;
      setEditorValue("Loading…");
      editorEls.contentBox.disabled = true;

      const r = await fetchJson(
        `${baseUrl}/files/content?path=${encodeURIComponent(path)}`
      );
      if (r.ok && r.body) {
        setEditorValue(r.body.content);
        editorEls.contentBox.disabled = false;
        updateByteCount();
        updateSaveEnabled();
        editorDialog.showModal();
        setTimeout(() => editorEls.contentBox.focus(), 0);
        return;
      }

      // Fallback: server says not editable. Re-open as binary.
      editorEls.contentBox.disabled = false;
      editor.mode = "binary";
    } else {
      editor.mode = "binary";
    }

    // Binary mode setup.
    editorEls.textPanel.hidden = true;
    editorEls.binaryPanel.hidden = false;
    editorEls.binarySize.textContent = "—"; // server gave us no size; cosmetic
    updateSaveEnabled();
    editorDialog.showModal();
    setTimeout(() => editorEls.filename.focus(), 0);
  }

  // ---------- direct-bound persistent-input listeners (legacy dialog) ----------

  if (editorDialog) {
    // Per plan: filename + content textarea input/keydown stay direct-
    // bound. They're high-frequency events on persistent inputs inside
    // the persistent legacy dialog; delegation would add per-keystroke
    // selector-matching overhead.
    editorEls.filename.addEventListener("input", () => {
      updateRenameHint();
      updateSaveEnabled();
    });
    editorEls.contentBox.addEventListener("input", () => {
      updateByteCount();
      updateSaveEnabled();
    });
    editorEls.contentBox.addEventListener("keydown", (event) => {
      if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
        event.preventDefault();
        editorEls.saveBtn.click();
      }
    });
    editorDialog.addEventListener("close", () => {
      setQueuedReplacement(null);
    });
  }

  // ---------- delegated handlers (legacy + URL-addressable) ----------

  function inLegacyEditor(el) {
    return !!editorDialog && editorDialog.contains(el);
  }
  function inRoutedEditor(el) {
    const mc = document.getElementById("modal-content");
    return !!mc && mc.contains(el);
  }

  // Replace-zone drag (legacy binary mode). Document-level delegation
  // gated on the zone being inside the legacy dialog.
  document.addEventListener("dragover", (event) => {
    const zone = event.target?.closest?.(".files-editor-replace-zone");
    if (!zone || !inLegacyEditor(zone)) return;
    if (Array.from(event.dataTransfer.types).includes("Files")) {
      event.preventDefault();
      zone.classList.add("is-drop-target");
    }
  });
  document.addEventListener("dragleave", (event) => {
    const zone = event.target?.closest?.(".files-editor-replace-zone");
    if (!zone || !inLegacyEditor(zone)) return;
    zone.classList.remove("is-drop-target");
  });
  document.addEventListener("drop", (event) => {
    const zone = event.target?.closest?.(".files-editor-replace-zone");
    if (!zone || !inLegacyEditor(zone)) return;
    if (!Array.from(event.dataTransfer.types).includes("Files")) return;
    event.preventDefault();
    zone.classList.remove("is-drop-target");
    const f = event.dataTransfer.files && event.dataTransfer.files[0];
    if (f) setQueuedReplacement(f);
  });

  // Replace-input change (browse-fallback): low frequency, delegated.
  document.addEventListener("change", (event) => {
    const input = event.target?.closest?.(".files-editor-replace-input");
    if (!input || !inLegacyEditor(input)) return;
    const f = input.files && input.files[0];
    if (f) setQueuedReplacement(f);
  });

  document.addEventListener("click", async (event) => {
    const target = event.target;
    if (!target) return;

    // Legacy: replace-clear button.
    const clearBtn = target.closest?.(".files-editor-replace-clear");
    if (clearBtn && inLegacyEditor(clearBtn)) {
      setQueuedReplacement(null);
      return;
    }
    // Legacy: replace-browse button → trigger hidden file input.
    const browseBtn = target.closest?.(".files-editor-replace-browse");
    if (browseBtn && inLegacyEditor(browseBtn)) {
      editorEls.replaceInput.click();
      return;
    }

    // Save (both editors).
    const saveBtn = target.closest?.(".files-editor-save");
    if (saveBtn) {
      if (inLegacyEditor(saveBtn)) {
        await legacySaveClicked();
        return;
      }
      if (inRoutedEditor(saveBtn)) {
        await routedSaveClicked();
        return;
      }
    }

    // Delete (both editors).
    const delBtn = target.closest?.(".files-editor-delete");
    if (delBtn) {
      if (inLegacyEditor(delBtn)) {
        await legacyDeleteClicked();
        return;
      }
      if (inRoutedEditor(delBtn)) {
        await routedDeleteClicked();
        return;
      }
    }
  });

  async function legacySaveClicked() {
    const filename = editorEls.filename.value.trim();
    if (!filename) return;
    if (filename.includes("/")) {
      alert("Filename can't contain '/'. Use drag-to-move to relocate.");
      return;
    }
    const folder = editor.folder || "";
    const newRel = joinPath(folder, filename);

    if (editor.creating) {
      // Text-flavor create → /save with no new_path.
      const r = await postJson(`${baseUrl}/files/save`, {
        path: newRel,
        content: getEditorValue(),
      });
      if (r.ok) {
        editorDialog.close();
        scheduleRefresh(folder);
      } else if (r.status === 409) {
        // askConflict lives in the legacy files-overlay.js for Phase A;
        // exposed there on __filesOverlay. Phase A Step 3 moves it to
        // dialogs.js (still on __filesOverlay), so this call site
        // doesn't change in Step 3.
        const askConflict = fo.askConflict;
        const withCollisionSuffix = fo.withCollisionSuffix;
        if (typeof askConflict !== "function" || typeof withCollisionSuffix !== "function") {
          alert(`Save failed (HTTP ${r.status}).`);
          return;
        }
        const action = await askConflict(newRel);
        if (action === "overwrite") {
          const r2 = await postJson(`${baseUrl}/files/save`, {
            path: newRel,
            content: getEditorValue(),
          });
          if (r2.ok) {
            editorDialog.close();
            scheduleRefresh(folder);
          } else {
            alert(
              (r2.body && r2.body.error) ||
                `Save failed (HTTP ${r2.status}).`
            );
          }
        } else if (action === "keep-both") {
          const altered = withCollisionSuffix(newRel);
          const r2 = await postJson(`${baseUrl}/files/save`, {
            path: altered,
            content: getEditorValue(),
          });
          if (r2.ok) {
            editorDialog.close();
            scheduleRefresh(folder);
          }
        }
      } else {
        alert((r.body && r.body.error) || `Save failed (HTTP ${r.status}).`);
      }
      return;
    }

    const renaming = newRel !== editor.originalPath;
    if (editor.mode === "text") {
      const payload = {
        path: editor.originalPath,
        content: getEditorValue(),
      };
      if (renaming) payload.new_path = newRel;
      const r = await postJson(`${baseUrl}/files/save`, payload);
      if (r.ok) {
        editorDialog.close();
        scheduleRefresh(folder);
      } else {
        alert(
          (r.body && r.body.error) || `Save failed (HTTP ${r.status}).`
        );
      }
      return;
    }

    // Binary mode.
    const fd = new FormData();
    fd.append("path", editor.originalPath);
    fd.append("csrf_token", csrfToken);
    if (renaming) fd.append("new_path", newRel);
    if (editor.queuedReplacement) {
      fd.append("file", editor.queuedReplacement);
      const r = await postForm(`${baseUrl}/files/replace`, fd);
      if (r.ok) {
        editorDialog.close();
        scheduleRefresh(folder);
      } else {
        alert(
          (r.body && r.body.error) || `Replace failed (HTTP ${r.status}).`
        );
      }
    } else if (renaming) {
      // Rename only via /move (no content change).
      const r = await postJson(`${baseUrl}/files/move`, {
        src: editor.originalPath,
        dst: newRel,
      });
      if (r.ok) {
        editorDialog.close();
        scheduleRefresh(folder);
      } else {
        alert(
          (r.body && r.body.error) || `Rename failed (HTTP ${r.status}).`
        );
      }
    }
  }

  async function legacyDeleteClicked() {
    if (!editor.originalPath) return;
    if (!confirm(`Delete ${editor.originalPath}?`)) return;
    const fd = new FormData();
    fd.append("path", editor.originalPath);
    fd.append("csrf_token", csrfToken);
    const r = await postForm(`${baseUrl}/files/delete`, fd);
    if (r.ok) {
      editorDialog.close();
      scheduleRefresh(parentOf(editor.originalPath));
    } else {
      alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`);
    }
  }

  // ---------- URL-addressable modal (#modal-content) save / delete ----------

  async function routedSaveClicked() {
    const modalContent = document.getElementById("modal-content");
    if (!modalContent) return;
    const ta = modalContent.querySelector("textarea[data-rel-path]");
    if (!ta) return;
    const relPath = ta.dataset.relPath;
    const filenameInput = modalContent.querySelector("[data-editor-filename]");
    const editedFilename = filenameInput ? filenameInput.value.trim() : "";
    const content = (window.__filesEditor && window.__filesEditor.getValue)
      ? window.__filesEditor.getValue()
      : ta.value;

    // is_new mode: relPath is empty; the new file's path comes from
    // data-at-folder + filename input. The server route at
    // /overlays/<id>/files/new sets data-at-folder; the /save endpoint
    // creates the file when the path doesn't already exist.
    if (!relPath) {
      if (!editedFilename) return; // empty filename — wait for input
      const atFolder = ta.dataset.atFolder || "";
      const fullPath = atFolder
        ? `${atFolder.replace(/\/+$/, "")}/${editedFilename}`
        : editedFilename;
      const r = await postJson(`${baseUrl}/files/save`, { path: fullPath, content });
      if (r.ok) {
        if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
        scheduleRefresh(parentOf(fullPath));
      } else if (r.status === 409) {
        alert(r.rawText || `A file at ${fullPath} already exists. Pick a different name.`);
      } else {
        alert((r.body && r.body.error) || r.rawText || `Create failed (HTTP ${r.status}).`);
      }
      return;
    }

    // Edit mode: rename-on-save if the filename input changed. Compose
    // a sibling rename (parent of relPath + new filename), send
    // payload.new_path so the server moves and writes atomically.
    const originalLeaf = relPath.split("/").pop() || relPath;
    const parent = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : "";
    let newPath = null;
    if (editedFilename && editedFilename !== originalLeaf) {
      newPath = parent ? `${parent}/${editedFilename}` : editedFilename;
    }

    const payload = { path: relPath, content };
    if (newPath) payload.new_path = newPath;

    const r = await postJson(`${baseUrl}/files/save`, payload);
    if (r.ok) {
      if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
      scheduleRefresh(parentOf(newPath || relPath));
    } else if (r.status === 409) {
      alert(r.rawText || `Conflict: destination already exists.`);
    } else {
      alert((r.body && r.body.error) || r.rawText || `Save failed (HTTP ${r.status}).`);
    }
  }

  async function routedDeleteClicked() {
    const modalContent = document.getElementById("modal-content");
    if (!modalContent) return;
    const ta = modalContent.querySelector("textarea[data-rel-path]");
    if (!ta) return;
    const relPath = ta.dataset.relPath;
    if (!relPath) return;
    if (!confirm(`Delete ${relPath}?`)) return;
    const fd = new FormData();
    fd.append("path", relPath);
    fd.append("csrf_token", csrfToken);
    const r = await postForm(`${baseUrl}/files/delete`, fd);
    if (r.ok) {
      if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
      scheduleRefresh(parentOf(relPath));
    } else {
      alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`);
    }
  }

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

  fo.registerHandler("new-file", (path) => {
    // Phase B Step 6: create-new-file uses the URL-addressable modal
    // (via the new GET /overlays/<id>/files/new?at=<folder> route).
    // The legacy openEditorTextNew remains in this file until Step 9
    // deletes the legacy dialog block wholesale; it's no longer
    // reachable from a user action.
    const url = `/overlays/${overlayId}/files/new?at=${encodeURIComponent(path)}`;
    if (typeof window.modals?.openRouted === "function") {
      window.modals.openRouted(url);
    } else {
      window.location.href = url;
    }
  });

  fo.registerHandler("edit", (path, actionEl) => {
    const editable = actionEl?.dataset?.editable === "1";
    if (editable) {
      // Editable text files: open via URL-addressable modal.
      const editUrl = `/overlays/${overlayId}/files/edit?path=${encodeURIComponent(path)}`;
      if (typeof window.modals?.openRouted === "function") {
        window.modals.openRouted(editUrl);
      } else {
        // Graceful fallback if modals.js didn't load — full-page nav
        // hits the same route and renders the standalone editor page.
        window.location.href = editUrl;
      }
      return;
    }
    // Binary files: legacy inline dialog (Phase B migrates this to
    // URL-addressable too).
    openEditorForFile(path, false);
  });
})();
