const state = {
  page: null,
  selected: null,
  clipboard: null,
};

const el = {
  regions: document.querySelector("#regions"),
  reload: document.querySelector("#reload"),
  save: document.querySelector("#save"),
  render: document.querySelector("#render"),
  status: document.querySelector("#status"),
  empty: document.querySelector("#empty-state"),
  form: document.querySelector("#block-form"),
  selectedRegion: document.querySelector("#selected-region"),
  selectedTitle: document.querySelector("#selected-title"),
  id: document.querySelector("#block-id"),
  label: document.querySelector("#block-label"),
  template: document.querySelector("#block-template"),
  anchor: document.querySelector("#block-anchor"),
  enabled: document.querySelector("#block-enabled"),
  contentFields: document.querySelector("#content-fields"),
  json: document.querySelector("#block-json"),
  moveUp: document.querySelector("#move-up"),
  moveDown: document.querySelector("#move-down"),
  duplicate: document.querySelector("#duplicate"),
  copy: document.querySelector("#copy"),
  pasteAfter: document.querySelector("#paste-after"),
  deleteBlock: document.querySelector("#delete-block"),
};

const STATIC_FALLBACK = {
  id: "static-elements",
  type: "staticElements",
  adminLabel: "Статичные элементы",
  enabled: true,
  content: {
    navigation: {
      logoAlt: "главная",
      brandLabel: "NODE.DC",
      links: [
        { label: "О продукте", href: "index.html#features", target: "" },
        { label: "Лист ожидания", href: "index.html#hero", target: "" },
        { label: "Документация", href: "https://docs.ssscript.app/", target: "_blank" },
        { label: "Тарифы", href: "index.html#pricing-wrap", target: "" },
      ],
    },
    notifications: [
      {
        eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> — ИЮЛЬ 2026',
        title: "Открытая бета",
        body: "Открываем доступ всем. Будем собирать и отлаживать вместе.",
      },
      {
        eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> —<span class="darker-70"> </span>ИЮЛЬ 2026',
        title: "Альфа для Founder-доступа",
        body: "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
      },
      {
        eyebrowHtml: "СЕЙЧАС",
        title: "Инвайты в закрытую бету уже отправлены.",
        body: "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
      },
    ],
    dock: {
      appTarget: "app-data",
      trashAlt: "Белая иконка корзины.",
      demoFileLabel: "демо-видео.txt",
    },
    assets: {
      webglModelSrc: "./assets/custom/icon.glb",
      logoPng: "./assets/custom/logo.png",
      logoPngAlt: "Логотип NODE.DC",
      appIconSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
      appIconAlt: "Иконка NODE.DC",
      appWordmarkSrc: "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
      appWordmarkAlt: "Логотип NODE.DC",
    },
  },
  editableFields: [
    field("content.navigation.logoAlt", "Логотип в шапке: alt", "text", "text", "Шапка"),
    field("content.navigation.brandLabel", "Бренд в шапке", "text", "text", "Шапка"),
    field("content.navigation.links.0.label", "Ссылка 1: текст", "text", "text", "Шапка"),
    field("content.navigation.links.0.href", "Ссылка 1: адрес", "url", "attr", "Шапка"),
    field("content.navigation.links.1.label", "Ссылка 2: текст", "text", "text", "Шапка"),
    field("content.navigation.links.1.href", "Ссылка 2: адрес", "url", "attr", "Шапка"),
    field("content.navigation.links.2.label", "Ссылка 3: текст", "text", "text", "Шапка"),
    field("content.navigation.links.2.href", "Ссылка 3: адрес", "url", "attr", "Шапка"),
    field("content.navigation.links.2.target", "Ссылка 3: target", "text", "attr", "Шапка"),
    field("content.navigation.links.3.label", "Ссылка 4: текст", "text", "text", "Шапка"),
    field("content.navigation.links.3.href", "Ссылка 4: адрес", "url", "attr", "Шапка"),
    field("content.notifications.0.eyebrowHtml", "Уведомление 1: дата/статус", "html", "html", "Уведомления"),
    field("content.notifications.0.title", "Уведомление 1: заголовок", "text", "text", "Уведомления"),
    field("content.notifications.0.body", "Уведомление 1: текст", "textarea", "text", "Уведомления"),
    field("content.notifications.1.eyebrowHtml", "Уведомление 2: дата/статус", "html", "html", "Уведомления"),
    field("content.notifications.1.title", "Уведомление 2: заголовок", "text", "text", "Уведомления"),
    field("content.notifications.1.body", "Уведомление 2: текст", "textarea", "text", "Уведомления"),
    field("content.notifications.2.eyebrowHtml", "Уведомление 3: дата/статус", "html", "html", "Уведомления"),
    field("content.notifications.2.title", "Уведомление 3: заголовок", "text", "text", "Уведомления"),
    field("content.notifications.2.body", "Уведомление 3: текст", "textarea", "text", "Уведомления"),
    field("content.dock.appTarget", "Dock: ID приложения", "text", "text", "Dock"),
    field("content.dock.trashAlt", "Dock: alt корзины", "text", "text", "Dock"),
    field("content.dock.demoFileLabel", "Dock: подпись демо-файла", "text", "text", "Dock"),
    field("content.assets.webglModelSrc", "WebGL-модель логотипа", "media", "attr", "Модель и логотипы"),
    field("content.assets.logoPng", "PNG-логотип", "media", "attr", "Модель и логотипы"),
    field("content.assets.logoPngAlt", "PNG-логотип: alt", "text", "text", "Модель и логотипы"),
    field("content.assets.appIconSrc", "Иконка приложения", "media", "attr", "Модель и логотипы"),
    field("content.assets.appIconAlt", "Иконка приложения: alt", "text", "text", "Модель и логотипы"),
    field("content.assets.appWordmarkSrc", "Wordmark приложения", "media", "attr", "Модель и логотипы"),
    field("content.assets.appWordmarkAlt", "Wordmark приложения: alt", "text", "text", "Модель и логотипы"),
  ],
};

const GROUP_COPY = {
  Шапка: "Навигация верхней mac-панели: бренд, ссылки и целевые адреса.",
  Уведомления: "Карточки уведомлений справа сверху. Порядок соответствует текущему stack в shell.",
  Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.",
  "Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.",
  Заголовок: "Главный заголовок и вводный текст выбранной секции.",
  "Тезисы первого экрана": "Четыре смысловых тезиса вокруг формы раннего доступа.",
  "Карточка раннего доступа": "Текст внутри центрального окна приложения.",
  Форма: "Плейсхолдеры, кнопки, success/failure состояния формы.",
  "Юридический блок": "Политики, права и нижний текст формы.",
  "Видео-окно": "Окно демо-видео и подключенный медиа-файл.",
  Пункты: "Текстовые пункты внутри секции.",
  Слайдер: "Видео-превью и подписи карточек внутри горизонтального слайдера.",
  "Один клик": "Подблок про быстрое добавление и настройку компонентов.",
  MCP: "Подблок про MCP-сервер и контекст Webflow.",
  FAQ: "Список вопросов и тексты ответов в mail-интерфейсе.",
  CTA: "Финальный призыв к действию: заголовок, пояснение, кнопка и ссылка.",
  Тарифы: "Верхняя часть тарифной таблицы: заголовки, планы, цены, кнопки и примечания.",
  Контент: "Остальные typed-поля выбранного блока.",
};

const MEDIA_ACCEPT = "image/*,video/*,.gif,.webm,.mov,.mp4,.m4v,.avi,.mkv,.glb,.gltf,.svg,.avif,.webp,.woff,.woff2";

function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null, options = {}) {
  return { path, label, kind, renderAs, group: group || inferFieldGroup(path), ...options };
}

function setStatus(message) {
  el.status.textContent = message;
}

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}

function getByPath(source, path) {
  return path.split(".").reduce((value, key) => value?.[key], source);
}

function setByPath(source, path, nextValue) {
  const keys = path.split(".");
  const lastKey = keys.pop();
  let target = source;

  keys.forEach((key, index) => {
    const nextKey = keys[index + 1] ?? lastKey;

    if (target[key] == null || typeof target[key] !== "object") {
      target[key] = /^\d+$/.test(nextKey) ? [] : {};
    }

    target = target[key];
  });

  target[lastKey] = nextValue;
}

function inferFieldGroup(path) {
  if (path.startsWith("content.navigation.")) return "Шапка";
  if (path.startsWith("content.notifications.")) return "Уведомления";
  if (path.startsWith("content.dock.")) return "Dock";
  if (path.startsWith("content.assets.")) return "Модель и логотипы";
  if (path.startsWith("content.cards.")) return "Тезисы первого экрана";
  if (path.startsWith("content.app.")) return "Карточка раннего доступа";
  if (path.startsWith("content.form.")) return "Форма";
  if (path === "content.legalHtml") return "Юридический блок";
  if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
  if (path.startsWith("content.features.")) return "Пункты";
  if (path.startsWith("content.sliderItems.")) return "Слайдер";
  if (path.startsWith("content.oneClick.")) return "Один клик";
  if (path.startsWith("content.mcp.")) return "MCP";
  if (path.startsWith("content.faq.")) return "FAQ";
  if (path.startsWith("content.cta.")) return "CTA";
  if (path.startsWith("content.plans.") || path.startsWith("content.pricing.")) return "Тарифы";
  if (path.startsWith("content.heading.") || path === "content.introHtml") return "Заголовок";
  return "Контент";
}

function ensureStaticElements() {
  if (!state.page.staticElements) {
    state.page.staticElements = clone(STATIC_FALLBACK);
    return;
  }

  state.page.staticElements.id ||= STATIC_FALLBACK.id;
  state.page.staticElements.type ||= STATIC_FALLBACK.type;
  state.page.staticElements.adminLabel ||= STATIC_FALLBACK.adminLabel;
  state.page.staticElements.enabled = true;
  state.page.staticElements.content ||= clone(STATIC_FALLBACK.content);
  state.page.staticElements.editableFields ||= clone(STATIC_FALLBACK.editableFields);
}

function isStaticSelection() {
  return state.selected?.kind === "static";
}

function activeRegion() {
  if (!state.selected) return null;

  if (isStaticSelection()) {
    return {
      id: "static",
      label: "Статичные элементы",
      description: "Shell-слой страницы: шапка, уведомления, dock и базовые ассеты.",
    };
  }

  return state.page.regions[state.selected.regionIndex];
}

function activeBlock() {
  if (!state.selected) return null;

  if (isStaticSelection()) {
    return state.page.staticElements;
  }

  const region = activeRegion();
  if (!region || state.selected.blockIndex == null) return null;
  return region.blocks[state.selected.blockIndex];
}

function uniqueId(base) {
  const ids = new Set([
    state.page.staticElements?.id,
    ...state.page.regions.flatMap((region) => region.blocks.map((block) => block.id)),
  ]);
  let candidate = `${base}-copy`;
  let index = 2;

  while (ids.has(candidate)) {
    candidate = `${base}-copy-${index}`;
    index += 1;
  }

  return candidate;
}

function truncateText(value, maxLength = 22) {
  if (!value) return "";
  return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
}

function fileNameFromUrl(value) {
  if (!value) return "Файл не выбран";

  try {
    const clean = value.split("?")[0].split("#")[0];
    return decodeURIComponent(clean.slice(clean.lastIndexOf("/") + 1)) || value;
  } catch {
    return value;
  }
}

function safeBucketSegment(value) {
  const safe = String(value || "site")
    .normalize("NFKD")
    .replace(/[^\w.-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .toLowerCase();

  return safe || "site";
}

function adminPreviewUrl(value) {
  if (!value) return "";
  if (/^(blob:|data:|https?:|\/)/i.test(value)) return value;
  if (value.startsWith("./")) return `.${value}`;
  return value;
}

function mediaKindFromValue(value) {
  if (!value) return null;
  if (/\.(mp4|webm|mov|m4v|avi|mkv)(\?.*)?$/i.test(value)) return "video";
  if (/\.(png|jpe?g|gif|svg|avif|webp)(\?.*)?$/i.test(value)) return "image";
  return null;
}

function uploadBucketForField(block, editableField) {
  const path = editableField.path.toLowerCase();
  const currentValue = String(getByPath(block, editableField.path) ?? "").toLowerCase();
  const blockSegment = safeBucketSegment(block?.id || block?.type || "site");
  let root = "images";

  if (path.includes("webgl") || currentValue.endsWith(".glb") || currentValue.endsWith(".gltf")) root = "models";
  if (path.includes("video") || path.includes("media") || path.includes("sliders") || path.includes("slideritems")) root = "media";
  if (path.includes("font") || /\.(woff2?|ttf|otf)(\?.*)?$/i.test(currentValue)) root = "fonts";

  return `${root}/${blockSegment}`;
}

function blockIcon(block) {
  const id = block.id || "";
  const type = block.type || "";

  if (id === "static-elements") return "SE";
  if (id.includes("hero")) return "H";
  if (id.includes("feature") || id.includes("video")) return "V";
  if (id.includes("component")) return "C";
  if (id.includes("modes")) return "AI";
  if (id.includes("faq")) return "Q";
  if (id.includes("pricing")) return "P";
  if (id.includes("waitlist")) return "W";
  if (id.includes("footer")) return "F";
  if (type.includes("Cta")) return "GO";
  return "B";
}

function readFileAsDataUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener("load", () => resolve(String(reader.result)));
    reader.addEventListener("error", () => reject(reader.error ?? new Error("Не удалось прочитать файл")));
    reader.readAsDataURL(file);
  });
}

function labelForRegion(region) {
  if (region.id === "beforeCViz") return "Главная";
  if (region.id === "cViz") return "Секции";
  return region.label || region.id;
}

function blockMeta(block) {
  return block.template || block.type || block.id;
}

function renderBlockButton({ list, block, meta, active, disabled, onClick }) {
  const button = document.createElement("button");
  const icon = document.createElement("span");
  const copy = document.createElement("span");
  const name = document.createElement("span");
  const description = document.createElement("span");

  button.type = "button";
  button.className = `block-btn${active ? " active" : ""}${disabled ? " disabled" : ""}`;
  icon.className = "block-icon";
  icon.textContent = blockIcon(block);
  copy.className = "block-copy";
  name.className = "block-name";
  name.textContent = block.adminLabel || block.id;
  description.className = "block-meta";
  description.textContent = meta;
  copy.append(name, description);
  button.append(icon, copy);
  button.addEventListener("click", onClick);
  list.append(button);
}

function renderRegions() {
  el.regions.innerHTML = "";

  const staticNode = document.createElement("section");
  const staticTitle = document.createElement("h3");
  const staticList = document.createElement("div");

  staticTitle.className = "region-title";
  staticTitle.textContent = "Static";
  staticList.className = "block-list";
  renderBlockButton({
    list: staticList,
    block: state.page.staticElements,
    meta: "shell · nav · notifications · dock",
    active: isStaticSelection(),
    disabled: false,
    onClick: () => {
      state.selected = { kind: "static" };
      renderAll();
    },
  });
  staticNode.append(staticTitle, staticList);
  el.regions.append(staticNode);

  state.page.regions.forEach((region, regionIndex) => {
    const regionNode = document.createElement("section");
    const title = document.createElement("h3");
    const list = document.createElement("div");

    title.className = "region-title";
    title.textContent = labelForRegion(region);
    list.className = "block-list";

    region.blocks.forEach((block, blockIndex) => {
      const isActive =
        state.selected?.kind === "block" &&
        state.selected?.regionIndex === regionIndex &&
        state.selected?.blockIndex === blockIndex;

      renderBlockButton({
        list,
        block,
        meta: blockMeta(block),
        active: isActive,
        disabled: block.enabled === false,
        onClick: () => {
          state.selected = { kind: "block", regionIndex, blockIndex };
          renderAll();
        },
      });
    });

    regionNode.append(title, list);
    el.regions.append(regionNode);
  });
}

function setStructuralControlsDisabled(disabled) {
  for (const control of [el.moveUp, el.moveDown, el.duplicate, el.copy, el.pasteAfter, el.deleteBlock]) {
    control.disabled = disabled;
  }
}

function renderEditor() {
  const region = activeRegion();
  const block = activeBlock();
  const isStatic = isStaticSelection();

  if (!block) {
    el.empty.classList.remove("hidden");
    el.form.classList.add("hidden");
    el.selectedRegion.textContent = "NODE.DC / Выберите блок";
    el.selectedTitle.textContent = "Контентный слой";
    return;
  }

  el.empty.classList.add("hidden");
  el.form.classList.remove("hidden");
  el.selectedRegion.textContent = `NODE.DC / ${region.label || region.id}`;
  el.selectedTitle.textContent = block.adminLabel || block.id;
  el.id.value = block.id || "";
  el.label.value = block.adminLabel || "";
  el.template.value = block.template || block.type || "";
  el.anchor.value = block.anchor || "";
  el.enabled.checked = block.enabled !== false;
  el.id.readOnly = isStatic;
  el.anchor.disabled = isStatic;
  el.enabled.disabled = isStatic;
  setStructuralControlsDisabled(isStatic);
  renderContentFields(block);
  el.json.value = JSON.stringify(block, null, 2);
}

function renderAll() {
  renderRegions();
  renderEditor();
}

function syncBlockFromFields() {
  const block = activeBlock();
  if (!block) return;

  syncContentFields();
  block.adminLabel = el.label.value.trim() || block.adminLabel || block.id;

  if (!isStaticSelection()) {
    block.id = el.id.value.trim();
    block.anchor = el.anchor.value.trim() || null;
    block.enabled = el.enabled.checked;
  }

  el.json.value = JSON.stringify(block, null, 2);
  renderRegions();
}

function groupEditableFields(block) {
  const groups = new Map();

  for (const editableField of block.editableFields ?? []) {
    const groupName = editableField.group || inferFieldGroup(editableField.path);
    if (!groups.has(groupName)) groups.set(groupName, []);
    groups.get(groupName).push(editableField);
  }

  return groups;
}

function renderMediaPreview(preview, value) {
  preview.innerHTML = "";

  const src = adminPreviewUrl(value);
  const kind = mediaKindFromValue(value);

  if (!src) {
    preview.textContent = "FILE";
    return;
  }

  if (kind === "video") {
    const video = document.createElement("video");
    video.src = src;
    video.autoplay = true;
    video.loop = true;
    video.muted = true;
    video.playsInline = true;
    preview.append(video);
    return;
  }

  if (kind === "image") {
    const image = document.createElement("img");
    image.src = src;
    image.alt = "";
    image.loading = "lazy";
    image.addEventListener("error", () => {
      preview.innerHTML = "";
      preview.textContent = "FILE";
    });
    preview.append(image);
    return;
  }

  preview.textContent = "FILE";
}

function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, value }) {
  hiddenInput.value = value;
  urlInput.value = value;
  fileName.textContent = truncateText(fileNameFromUrl(value), 28);
  fileName.title = fileNameFromUrl(value);
  setByPath(block, editableField.path, value);
  renderMediaPreview(preview, value);

  if (activeBlock() === block) {
    el.json.value = JSON.stringify(block, null, 2);
  }
}

function renderMediaField({ block, editableField, grid }) {
  const value = getByPath(block, editableField.path) ?? "";
  const container = document.createElement("div");
  const labelRow = document.createElement("div");
  const labelText = document.createElement("span");
  const kind = document.createElement("span");
  const hiddenInput = document.createElement("input");
  const control = document.createElement("div");
  const fileControl = document.createElement("div");
  const fileLabel = document.createElement("label");
  const fileName = document.createElement("span");
  const fileInput = document.createElement("input");
  const urlInput = document.createElement("input");
  const sourceSwitch = document.createElement("div");
  const fileSourceButton = document.createElement("button");
  const urlSourceButton = document.createElement("button");
  const preview = document.createElement("div");
  const path = document.createElement("span");
  const error = document.createElement("span");
  const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`;
  let source = /^(https?:)?\/\//i.test(value) ? "url" : "file";

  function setSource(nextSource) {
    source = nextSource;
    fileControl.hidden = source !== "file";
    urlInput.hidden = source !== "url";
    fileSourceButton.dataset.active = String(source === "file");
    urlSourceButton.dataset.active = String(source === "url");
  }

  container.className = "field-control wide service-media-field";
  labelRow.className = "field-label-row";
  labelText.className = "field-label-text";
  labelText.textContent = editableField.label || editableField.path;
  kind.className = "field-kind";
  kind.textContent = editableField.kind || "media";
  labelRow.append(labelText, kind);

  hiddenInput.className = "service-media-hidden-input";
  hiddenInput.type = "hidden";
  hiddenInput.dataset.path = editableField.path;
  hiddenInput.value = value;

  control.className = "service-media-control";

  fileControl.className = "service-media-file-control";
  fileLabel.className = "service-media-file-button";
  fileLabel.htmlFor = inputId;
  fileLabel.textContent = "Выберите файл";
  fileName.className = "service-media-file-name";
  fileName.textContent = truncateText(fileNameFromUrl(value), 28);
  fileName.title = fileNameFromUrl(value);
  fileInput.id = inputId;
  fileInput.type = "file";
  fileInput.accept = MEDIA_ACCEPT;
  fileControl.append(fileLabel, fileName, fileInput);

  urlInput.className = "service-media-url-input";
  urlInput.type = "text";
  urlInput.placeholder = "https://...";
  urlInput.autocomplete = "off";
  urlInput.value = value;

  sourceSwitch.className = "service-media-source-switch";
  sourceSwitch.setAttribute("aria-label", `${editableField.label || editableField.path}: источник`);
  fileSourceButton.className = "service-media-source-button";
  fileSourceButton.type = "button";
  fileSourceButton.title = "Файл с диска";
  fileSourceButton.setAttribute("aria-label", "Файл с диска");
  fileSourceButton.textContent = "HD";
  urlSourceButton.className = "service-media-source-button";
  urlSourceButton.type = "button";
  urlSourceButton.title = "Внешняя ссылка";
  urlSourceButton.setAttribute("aria-label", "Внешняя ссылка");
  urlSourceButton.textContent = "URL";
  sourceSwitch.append(fileSourceButton, urlSourceButton);

  preview.className = "service-media-preview";
  renderMediaPreview(preview, value);

  path.className = "field-path";
  path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`;
  error.className = "media-upload-error";

  fileSourceButton.addEventListener("click", () => setSource("file"));
  urlSourceButton.addEventListener("click", () => {
    setSource("url");
    urlInput.focus();
  });

  urlInput.addEventListener("input", () => {
    updateMediaFieldValue({
      block,
      editableField,
      hiddenInput,
      urlInput,
      fileName,
      preview,
      value: urlInput.value,
    });
  });

  fileInput.addEventListener("change", async () => {
    const file = fileInput.files?.[0];
    if (!file) return;

    error.textContent = "";
    fileName.textContent = "Сохраняем в assets...";

    try {
      const dataUrl = await readFileAsDataUrl(file);
      const response = await fetch("/api/upload/asset", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          fileName: file.name,
          mimeType: file.type || "application/octet-stream",
          bucket: uploadBucketForField(block, editableField),
          dataUrl,
        }),
      });
      const payload = await response.json();

      if (!response.ok || !payload.ok) {
        throw new Error(payload.error || "Файл не удалось сохранить");
      }

      updateMediaFieldValue({
        block,
        editableField,
        hiddenInput,
        urlInput,
        fileName,
        preview,
        value: payload.url,
      });
      setSource("file");
      setStatus(`Файл сохранён: ${payload.url}. Нажмите «Сохранить JSON», чтобы записать путь в content/pages/home.json.`);
    } catch (uploadError) {
      error.textContent = uploadError.message;
      fileName.textContent = truncateText(fileNameFromUrl(hiddenInput.value), 28);
      setStatus(`Ошибка загрузки: ${uploadError.message}`);
    } finally {
      fileInput.value = "";
    }
  });

  setSource(source);
  control.append(fileControl, urlInput, sourceSwitch, preview);
  container.append(labelRow, hiddenInput, control, path, error);
  grid.append(container);
}

function renderContentFields(block) {
  el.contentFields.innerHTML = "";

  if (!block.editableFields?.length) {
    const note = document.createElement("div");
    note.className = "empty-note";
    note.textContent = "Для этого блока typed-поля ещё не выделены. Пока доступно редактирование через JSON.";
    el.contentFields.append(note);
    return;
  }

  for (const [groupName, fields] of groupEditableFields(block)) {
    const group = document.createElement("section");
    const head = document.createElement("div");
    const titleWrap = document.createElement("div");
    const title = document.createElement("h3");
    const desc = document.createElement("p");
    const grid = document.createElement("div");

    group.className = "content-group";
    head.className = "group-head";
    title.textContent = groupName;
    desc.className = "group-desc";
    desc.textContent = GROUP_COPY[groupName] || GROUP_COPY["Контент"];
    titleWrap.append(title, desc);
    head.append(titleWrap);
    grid.className = "group-grid";

    for (const editableField of fields) {
      if (editableField.kind === "media") {
        renderMediaField({ block, editableField, grid });
        continue;
      }

      const label = document.createElement("label");
      const labelRow = document.createElement("span");
      const labelText = document.createElement("span");
      const kind = document.createElement("span");
      const path = document.createElement("span");
      const value = getByPath(block, editableField.path) ?? "";
      const isLong = editableField.kind === "html" || editableField.kind === "textarea";
      const input = isLong ? document.createElement("textarea") : document.createElement("input");

      label.className = `field-control${isLong ? " wide" : ""}`;
      labelRow.className = "field-label-row";
      labelText.className = "field-label-text";
      labelText.textContent = editableField.label || editableField.path;
      kind.className = "field-kind";
      kind.textContent = editableField.kind || "text";
      path.className = "field-path";
      path.textContent = editableField.path;
      labelRow.append(labelText, kind);

      input.dataset.path = editableField.path;
      input.value = value;

      if (!isLong) {
        input.type = "text";
        input.autocomplete = "off";
      }

      input.addEventListener("input", () => {
        const active = activeBlock();
        if (!active) return;
        setByPath(active, editableField.path, input.value);
        el.json.value = JSON.stringify(active, null, 2);
      });

      label.append(labelRow, input, path);
      grid.append(label);
    }

    group.append(head, grid);
    el.contentFields.append(group);
  }
}

function syncContentFields() {
  const block = activeBlock();
  if (!block) return;

  for (const input of el.contentFields.querySelectorAll("[data-path]")) {
    setByPath(block, input.dataset.path, input.value);
  }
}

function syncBlockFromJson() {
  if (!state.selected) return false;

  try {
    const parsed = JSON.parse(el.json.value);
    if (isStaticSelection()) {
      state.page.staticElements = parsed;
    } else {
      const region = activeRegion();
      if (!region) return false;
      region.blocks[state.selected.blockIndex] = parsed;
    }
    renderAll();
    setStatus("JSON блока применён локально. Нажмите «Сохранить JSON», чтобы записать файл.");
    return true;
  } catch (error) {
    setStatus(`Ошибка JSON: ${error.message}`);
    return false;
  }
}

async function loadPage() {
  const response = await fetch("/api/page/home");
  if (!response.ok) throw new Error(await response.text());
  state.page = await response.json();
  ensureStaticElements();
  state.selected = { kind: "static" };
  renderAll();
  setStatus("Модель home.json загружена.");
}

async function savePage() {
  if (activeBlock()) syncBlockFromFields();

  const response = await fetch("/api/page/home", {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(state.page),
  });

  if (!response.ok) throw new Error(await response.text());
  setStatus("content/pages/home.json сохранён.");
}

async function renderHome() {
  const response = await fetch("/api/render/home", { method: "POST" });
  const payload = await response.json();

  if (!response.ok || !payload.ok) {
    throw new Error(payload.error || "Render failed");
  }

  setStatus(payload.stdout || "index.html собран.");
}

function moveSelected(delta) {
  if (isStaticSelection()) return;
  const region = activeRegion();
  const index = state.selected?.blockIndex;
  if (!region || index == null) return;

  const nextIndex = index + delta;
  if (nextIndex < 0 || nextIndex >= region.blocks.length) return;

  const [block] = region.blocks.splice(index, 1);
  region.blocks.splice(nextIndex, 0, block);
  state.selected.blockIndex = nextIndex;
  renderAll();
}

function duplicateSelected() {
  if (isStaticSelection()) return;
  const region = activeRegion();
  const block = activeBlock();
  if (!region || !block) return;

  const duplicate = clone(block);
  duplicate.id = uniqueId(block.id);
  duplicate.adminLabel = `${block.adminLabel || block.id} — копия`;
  duplicate.scopeIds = true;
  region.blocks.splice(state.selected.blockIndex + 1, 0, duplicate);
  state.selected.blockIndex += 1;
  renderAll();
}

function copySelected() {
  if (isStaticSelection()) return;
  const block = activeBlock();
  if (!block) return;

  state.clipboard = clone(block);
  setStatus(`Скопирован блок: ${block.adminLabel || block.id}`);
}

function pasteAfterSelected() {
  if (isStaticSelection()) return;
  const region = activeRegion();
  if (!region || !state.clipboard) return;

  const pasted = clone(state.clipboard);
  pasted.id = uniqueId(pasted.id);
  pasted.adminLabel = `${pasted.adminLabel || pasted.id} — вставка`;
  pasted.scopeIds = true;
  region.blocks.splice(state.selected.blockIndex + 1, 0, pasted);
  state.selected.blockIndex += 1;
  renderAll();
}

function deleteSelected() {
  if (isStaticSelection()) return;
  const region = activeRegion();
  if (!region || state.selected.blockIndex == null) return;

  region.blocks.splice(state.selected.blockIndex, 1);
  state.selected.blockIndex = Math.min(state.selected.blockIndex, region.blocks.length - 1);
  if (state.selected.blockIndex < 0) state.selected = { kind: "static" };
  renderAll();
}

for (const input of [el.id, el.label, el.anchor, el.enabled]) {
  input.addEventListener("input", syncBlockFromFields);
  input.addEventListener("change", syncBlockFromFields);
}

el.json.addEventListener("blur", syncBlockFromJson);
el.reload.addEventListener("click", () => loadPage().catch((error) => setStatus(error.message)));
el.save.addEventListener("click", () => savePage().catch((error) => setStatus(error.message)));
el.render.addEventListener("click", () => renderHome().catch((error) => setStatus(error.message)));
el.moveUp.addEventListener("click", () => moveSelected(-1));
el.moveDown.addEventListener("click", () => moveSelected(1));
el.duplicate.addEventListener("click", duplicateSelected);
el.copy.addEventListener("click", copySelected);
el.pasteAfter.addEventListener("click", pasteAfterSelected);
el.deleteBlock.addEventListener("click", deleteSelected);

loadPage().catch((error) => setStatus(error.message));
