const state = {
  page: null,
  templates: [],
  selected: null,
  clipboard: null,
  drag: null,
  dragTarget: null,
  dirty: false,
  filePickerActive: false,
  mediaUploadActive: false,
  filePickerResetTimer: null,
};

const el = {
  regions: document.querySelector("#regions"),
  addBlock: document.querySelector("#add-block"),
  reload: document.querySelector("#reload"),
  save: document.querySelector("#save"),
  render: document.querySelector("#render"),
  dirtyState: document.querySelector("#dirty-state"),
  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"),
  templateModal: document.querySelector("#template-modal"),
  templateList: document.querySelector("#template-list"),
  templateModalClose: document.querySelector("#template-modal-close"),
  templateModalCancel: document.querySelector("#template-modal-cancel"),
};

const STATIC_FALLBACK = {
  id: "static-elements",
  type: "staticElements",
  adminLabel: "Статичные элементы",
  enabled: true,
  content: {
    navigation: {
      logoSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
      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',
        eyebrowClass: "red-txt gray",
        title: "Открытая бета",
        body: "Открываем доступ всем. Будем собирать и отлаживать вместе.",
      },
      {
        eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> —<span class="darker-70"> </span>ИЮЛЬ 2026',
        eyebrowClass: "red-txt gray",
        title: "Альфа для Founder-доступа",
        body: "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
      },
      {
        eyebrowHtml: "СЕЙЧАС",
        eyebrowClass: "red-txt",
        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.logoSrc", "Логотип в шапке", "media", "attr", "Шапка: логотип"),
    field("content.navigation.logoAlt", "Логотип в шапке: alt", "text", "text", "Шапка: логотип"),
    field("content.navigation.brandLabel", "Бренд в шапке", "text", "text", "Шапка: логотип"),
    collection(
      "content.navigation.links",
      "Кнопки шапки",
      "Шапка: ссылки",
      { label: "Новая ссылка", href: "index.html#", target: "" },
      [
        field("label", "Текст кнопки", "text", "text", "Шапка: ссылки"),
        field("href", "Адрес", "url", "attr", "Шапка: ссылки"),
        field("target", "Target", "text", "attr", "Шапка: ссылки"),
      ],
    ),
    collection(
      "content.notifications",
      "Уведомления",
      "Уведомления",
      {
        eyebrowHtml: "СЕЙЧАС",
        eyebrowClass: "red-txt gray",
        title: "Новое уведомление",
        body: "Текст уведомления.",
      },
      [
        field("eyebrowHtml", "Дата / статус", "html", "html", "Уведомления"),
        field("eyebrowClass", "CSS-класс статуса", "text", "attr", "Уведомления"),
        field("title", "Заголовок", "text", "text", "Уведомления"),
        field("body", "Текст", "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-панели: файл логотипа, alt и брендовая подпись.",
  "Шапка: ссылки": "Пункты навигации верхней mac-панели. Каждый пункт можно добавить, удалить и переставить.",
  Уведомления: "Карточки уведомлений справа сверху. Порядок в админке соответствует порядку на сайте.",
  Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.",
  "Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.",
  Заголовок: "Главный заголовок и вводный текст выбранной секции.",
  "Тезисы первого экрана": "Четыре смысловых тезиса вокруг формы раннего доступа.",
  "Карточка раннего доступа": "Текст внутри центрального окна приложения.",
  Форма: "Плейсхолдеры, кнопки, success/failure состояния формы.",
  "Юридический блок": "Политики, права и нижний текст формы.",
  "Видео-окно": "Окно демо-видео и подключенный медиа-файл.",
  Пункты: "Текстовые пункты внутри секции.",
  Слайдер: "Видео-превью и подписи карточек внутри горизонтального слайдера.",
  "Один клик": "Подблок про быстрое добавление и настройку компонентов.",
  MCP: "Подблок про MCP-сервер и контекст Webflow.",
  FAQ: "Список вопросов и тексты ответов в mail-интерфейсе.",
  CTA: "Финальный призыв к действию: заголовок, пояснение, кнопка и ссылка.",
  Тарифы: "Верхняя часть тарифной таблицы: заголовки, планы, цены, кнопки и примечания.",
  "Строки тарифов": "Повторяемые строки тарифной таблицы и значения по каждому плану.",
  "Медиа-галерея": "Окна с изображениями, подписи файлов и связанные иконки.",
  "TXT-файлы": "Список файлов листа ожидания: название окна, подпись и текст внутри файла.",
  Футер: "Нижняя навигация, контактные ссылки, политики и брендовый блок.",
  Контент: "Остальные typed-поля выбранного блока.",
};

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

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 collection(path, label, group, itemTemplate, itemFields) {
  return {
    path,
    label,
    kind: "collection",
    renderAs: "collection",
    group,
    itemTemplate,
    itemFields,
  };
}

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

function setDirty(isDirty, message = null) {
  state.dirty = isDirty;
  el.dirtyState.dataset.dirty = String(isDirty);
  el.dirtyState.textContent = isDirty ? "Есть несохранённые изменения" : "Синхронизировано";

  if (message) {
    setStatus(message);
  }
}

function markDirty(message = null) {
  setDirty(true, message);
}

function createMediaFilePicker() {
  const input = document.createElement("input");
  input.type = "file";
  input.accept = MEDIA_ACCEPT;
  input.className = "admin-file-picker-input";
  input.tabIndex = -1;
  input.setAttribute("aria-hidden", "true");
  document.body.append(input);
  return input;
}

function setFilePickerActive(active) {
  state.filePickerActive = active;

  if (active) {
    document.body.dataset.filePickerActive = "true";
  } else {
    delete document.body.dataset.filePickerActive;
  }
}

function recoverEditorSurface() {
  if (!activeBlock()) return;
  el.empty.classList.add("hidden");
  el.form.classList.remove("hidden");
}

function scheduleFilePickerRecovery(delay = 360) {
  window.clearTimeout(state.filePickerResetTimer);
  state.filePickerResetTimer = window.setTimeout(() => {
    if (state.mediaUploadActive) return;
    setFilePickerActive(false);
    recoverEditorSurface();
  }, delay);
}

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.pricingCells.")) return "Строки тарифов";
  if (path.startsWith("content.galleryItems.")) return "Медиа-галерея";
  if (path.startsWith("content.files.")) return "TXT-файлы";
  if (path.startsWith("content.footer.")) 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.content.navigation ||= clone(STATIC_FALLBACK.content.navigation);
  state.page.staticElements.content.navigation.logoSrc ||= STATIC_FALLBACK.content.navigation.logoSrc;
  state.page.staticElements.content.navigation.links ||= clone(STATIC_FALLBACK.content.navigation.links);
  state.page.staticElements.content.notifications ||= clone(STATIC_FALLBACK.content.notifications);
  state.page.staticElements.content.notifications.forEach((notification, index) => {
    notification.eyebrowClass ||= STATIC_FALLBACK.content.notifications[index]?.eyebrowClass || "red-txt gray";
  });
  state.page.staticElements.content.dock ||= clone(STATIC_FALLBACK.content.dock);
  state.page.staticElements.content.assets ||= clone(STATIC_FALLBACK.content.assets);
  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 uniqueBlockId(base) {
  return uniqueId(safeBucketSegment(base || "block").replace(/-copy(?:-\d+)?$/, ""));
}

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 shouldWarnAboutSolidLogoBackground(editableField) {
  const path = String(editableField?.path || "").toLowerCase();
  const label = String(editableField?.label || "").toLowerCase();
  return /logo|icon|wordmark|логотип|икон/.test(`${path} ${label}`);
}

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 cleanupDragState() {
  state.drag = null;
  state.dragTarget = null;
  document.querySelectorAll(".dragging, .drag-over, .sorting-source, .sorting-active").forEach((node) => {
    node.classList.remove("dragging", "drag-over", "sorting-source", "sorting-active");
  });
  document.querySelectorAll(".sortable-ghost, .sortable-placeholder").forEach((node) => node.remove());
}

function createSortableGhost(source, event) {
  const rect = source.getBoundingClientRect();
  const ghost = source.cloneNode(true);

  ghost.classList.add("sortable-ghost");
  ghost.style.width = `${rect.width}px`;
  ghost.style.height = `${rect.height}px`;
  ghost.dataset.dragOffsetX = String(event.clientX - rect.left);
  ghost.dataset.dragOffsetY = String(event.clientY - rect.top);
  document.body.append(ghost);
  moveSortableGhost(ghost, event);

  return ghost;
}

function moveSortableGhost(ghost, event) {
  ghost.style.left = `${event.clientX - Number(ghost.dataset.dragOffsetX || 0)}px`;
  ghost.style.top = `${event.clientY - Number(ghost.dataset.dragOffsetY || 0)}px`;
}

function createSortablePlaceholder(source) {
  const rect = source.getBoundingClientRect();
  const placeholder = document.createElement("div");

  placeholder.className = "sortable-placeholder";
  placeholder.style.height = `${rect.height}px`;
  return placeholder;
}

function pointInsideRect(event, rect) {
  return event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom;
}

function movePlaceholderByPoint({ event, list, source, placeholder, selector }) {
  const rect = list.getBoundingClientRect();
  if (!pointInsideRect(event, rect)) return;

  const candidates = [...list.querySelectorAll(selector)].filter((node) => node !== source && node !== placeholder);

  for (const candidate of candidates) {
    const candidateRect = candidate.getBoundingClientRect();
    if (event.clientY < candidateRect.top + candidateRect.height / 2) {
      list.insertBefore(placeholder, candidate);
      return;
    }
  }

  list.append(placeholder);
}

function sortablePlaceholderIndex({ list, placeholder, source, selector }) {
  return [...list.querySelectorAll(`${selector}, .sortable-placeholder`)]
    .filter((node) => node === placeholder || node !== source)
    .indexOf(placeholder);
}

function moveBlockWithinRegionToIndex(regionIndex, fromIndex, toIndex) {
  const region = state.page.regions[regionIndex];
  if (!region || fromIndex === toIndex || toIndex < 0) {
    cleanupDragState();
    return;
  }

  const [block] = region.blocks.splice(fromIndex, 1);
  region.blocks.splice(Math.min(toIndex, region.blocks.length), 0, block);
  state.selected = { kind: "block", regionIndex, blockIndex: Math.min(toIndex, region.blocks.length - 1) };
  cleanupDragState();
  renderAll();
  markDirty("Порядок блоков изменён локально. Нажмите «Сохранить модель» или «Обновить index.html».");
}

function startPointerDrag(event, button, dragContext) {
  if (event.button !== 0) return;
  event.preventDefault();
  event.stopPropagation();

  const list = button.closest(".block-list");
  if (!list) return;

  const placeholder = createSortablePlaceholder(button);
  const ghost = createSortableGhost(button, event);

  state.drag = dragContext;
  state.dragTarget = dragContext;
  button.classList.add("dragging", "sorting-source");
  list.classList.add("sorting-active");
  button.after(placeholder);

  const onMove = (moveEvent) => {
    moveSortableGhost(ghost, moveEvent);
    movePlaceholderByPoint({
      event: moveEvent,
      list,
      source: button,
      placeholder,
      selector: ".block-btn.has-drag-handle",
    });
  };

  const onFinish = () => {
    document.removeEventListener("pointermove", onMove);
    document.removeEventListener("pointerup", onFinish);
    document.removeEventListener("pointercancel", onCancel);

    const nextIndex = sortablePlaceholderIndex({
      list,
      placeholder,
      source: button,
      selector: ".block-btn.has-drag-handle",
    });
    moveBlockWithinRegionToIndex(dragContext.regionIndex, dragContext.blockIndex, nextIndex);
  };

  const onCancel = () => {
    document.removeEventListener("pointermove", onMove);
    document.removeEventListener("pointerup", onFinish);
    document.removeEventListener("pointercancel", onCancel);
    cleanupDragState();
  };

  document.addEventListener("pointermove", onMove);
  document.addEventListener("pointerup", onFinish);
  document.addEventListener("pointercancel", onCancel);
}

function reorderBlockWithinRegion(from, to) {
  if (!from || from.regionIndex !== to.regionIndex || from.blockIndex === to.blockIndex) {
    cleanupDragState();
    return;
  }

  const region = state.page.regions[from.regionIndex];
  if (!region) {
    cleanupDragState();
    return;
  }

  const [block] = region.blocks.splice(from.blockIndex, 1);
  let insertIndex = to.blockIndex;
  if (from.blockIndex < to.blockIndex) insertIndex -= 1;
  region.blocks.splice(insertIndex, 0, block);
  state.selected = { kind: "block", regionIndex: from.regionIndex, blockIndex: insertIndex };
  cleanupDragState();
  renderAll();
  markDirty("Порядок блоков изменён локально. Нажмите «Сохранить модель» или «Обновить index.html».");
}

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

  button.role = "button";
  button.tabIndex = 0;
  button.className = `block-btn${active ? " active" : ""}${disabled ? " disabled" : ""}${canDrag ? " has-drag-handle" : ""}`;
  button.dataset.adminBlockId = block.id || "";
  button.dataset.adminBlockType = block.type || "";
  button.dataset.adminBlockTemplate = block.template || "";
  if (canDrag) {
    button.dataset.regionIndex = String(dragContext.regionIndex);
    button.dataset.blockIndex = String(dragContext.blockIndex);
  }
  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);

  if (canDrag) {
    const handle = document.createElement("span");
    handle.className = "block-drag-handle";
    handle.draggable = false;
    handle.title = "Перетащить блок";
    handle.setAttribute("aria-label", "Перетащить блок");
    handle.textContent = "⋮⋮";
    handle.addEventListener("click", (event) => event.stopPropagation());
    handle.addEventListener("pointerdown", (event) => startPointerDrag(event, button, dragContext));
    handle.addEventListener("dragstart", (event) => {
      state.drag = dragContext;
      button.classList.add("dragging");
      event.dataTransfer.effectAllowed = "move";
      event.dataTransfer.setData("text/plain", `${dragContext.regionIndex}:${dragContext.blockIndex}`);
    });
    handle.addEventListener("dragend", cleanupDragState);
    handle.addEventListener("keydown", (event) => event.stopPropagation());
    button.append(handle);
  }

  if (canDrag) {
    button.addEventListener("dragover", (event) => {
      if (!state.drag || state.drag.regionIndex !== dragContext.regionIndex) return;
      event.preventDefault();
      event.dataTransfer.dropEffect = "move";
      button.classList.add("drag-over");
    });
    button.addEventListener("dragleave", () => button.classList.remove("drag-over"));
    button.addEventListener("drop", (event) => {
      event.preventDefault();
      button.classList.remove("drag-over");
      reorderBlockWithinRegion(state.drag, dragContext);
    });
  }

  button.addEventListener("click", onClick);
  button.addEventListener("keydown", (event) => {
    if (event.key !== "Enter" && event.key !== " ") return;
    event.preventDefault();
    onClick(event);
  });
  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,
        dragContext: { regionIndex, blockIndex },
        onClick: () => {
          state.selected = { kind: "block", regionIndex, blockIndex };
          renderAll();
        },
      });
    });

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

function blockTemplates() {
  return state.templates.map((template) => {
    const regionIndex = state.page.regions.findIndex((region) => region.id === template.regionId);
    const region = state.page.regions[regionIndex] || state.page.regions[0];

    return {
      ...template,
      regionIndex: regionIndex >= 0 ? regionIndex : 0,
      regionLabel: template.regionLabel || labelForRegion(region),
    };
  });
}

function renderTemplateModal() {
  el.templateList.innerHTML = "";

  for (const template of blockTemplates()) {
    const card = document.createElement("button");
    const icon = document.createElement("span");
    const copy = document.createElement("span");
    const title = document.createElement("span");
    const meta = document.createElement("span");
    const region = document.createElement("span");

    card.type = "button";
    card.className = "template-card";
    icon.className = "template-card-icon";
    icon.textContent = blockIcon(template.block);
    copy.className = "template-card-copy";
    title.className = "template-card-title";
    title.textContent = template.block.adminLabel || template.block.id;
    meta.className = "template-card-meta";
    meta.textContent = blockMeta(template.block);
    region.className = "template-card-region";
    region.textContent = `Добавится в: ${template.regionLabel}`;
    copy.append(title, meta, region);
    card.append(icon, copy);
    card.addEventListener("click", () => addBlockFromTemplate(template.key));
    el.templateList.append(card);
  }
}

function openTemplateModal() {
  renderTemplateModal();
  el.templateModal.classList.remove("hidden");
  el.templateList.querySelector("button")?.focus();
}

function closeTemplateModal() {
  el.templateModal.classList.add("hidden");
  el.addBlock.focus();
}

function addBlockFromTemplate(templateKey) {
  const template = blockTemplates().find((candidate) => candidate.key === templateKey);
  if (!template) return;

  const region = state.page.regions[template.regionIndex];
  const block = clone(template.block);

  block.id = uniqueBlockId(block.id);
  block.enabled = true;
  block.scopeIds = true;
  region.blocks.push(block);
  state.selected = { kind: "block", regionIndex: template.regionIndex, blockIndex: region.blocks.length - 1 };
  closeTemplateModal();
  renderAll();
  markDirty(`Добавлен блок «${block.adminLabel || block.id}». Нажмите «Сохранить модель» или «Обновить index.html».`);
}

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

function renderEditorError(error) {
  console.error(error);
  el.empty.classList.add("hidden");
  el.form.classList.remove("hidden");
  el.contentFields.innerHTML = "";

  const note = document.createElement("div");
  note.className = "empty-note editor-error-note";
  note.textContent = `Редактор не смог отрисовать поля блока: ${error.message}`;
  el.contentFields.append(note);
  setStatus(`Ошибка редактора: ${error.message}`);
}

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.form.dataset.static = String(isStatic);
  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;
  el.enabled.closest(".toggle-row").hidden = isStatic;
  setStructuralControlsDisabled(isStatic);

  try {
    renderContentFields(block);
  } catch (error) {
    renderEditorError(error);
  }

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

function renderAll() {
  try {
    renderRegions();
    renderEditor();
  } catch (error) {
    console.error(error);
    setStatus(`Ошибка рендера админки: ${error.message}`);
  }
}

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();
  markDirty();
}

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 resetMediaPreview(preview, hint) {
  preview.classList.remove("is-image", "is-video", "is-solid-dark");
  preview.innerHTML = "";
  preview.title = "";
  if (hint) hint.textContent = "";
}

function analyzeLogoPreview(image, preview, hint, editableField) {
  if (!hint || !shouldWarnAboutSolidLogoBackground(editableField)) return;

  try {
    const sampleSize = 24;
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d", { willReadFrequently: true });
    if (!context) return;

    canvas.width = sampleSize;
    canvas.height = sampleSize;
    context.drawImage(image, 0, 0, sampleSize, sampleSize);
    const data = context.getImageData(0, 0, sampleSize, sampleSize).data;
    let opaque = 0;
    let edgeOpaque = 0;
    let edgeDark = 0;

    for (let y = 0; y < sampleSize; y += 1) {
      for (let x = 0; x < sampleSize; x += 1) {
        const offset = (y * sampleSize + x) * 4;
        const alpha = data[offset + 3];
        if (alpha < 240) continue;

        opaque += 1;
        const isEdge = x <= 1 || y <= 1 || x >= sampleSize - 2 || y >= sampleSize - 2;
        const isDark = data[offset] + data[offset + 1] + data[offset + 2] < 66;

        if (isEdge) {
          edgeOpaque += 1;
          if (isDark) edgeDark += 1;
        }
      }
    }

    const total = sampleSize * sampleSize;
    const edgeRatio = edgeOpaque ? edgeDark / edgeOpaque : 0;
    const opaqueRatio = opaque / total;

    if (opaqueRatio > 0.92 && edgeRatio > 0.82) {
      preview.classList.add("is-solid-dark");
      hint.textContent = "Файл с непрозрачным тёмным фоном. Для логотипа на тёмном интерфейсе лучше PNG/SVG с прозрачностью.";
    }
  } catch {
    // External images can be blocked by canvas CORS. Preview still works, just without diagnostics.
  }
}

function renderMediaPreview(preview, value, hint = null, editableField = null) {
  resetMediaPreview(preview, hint);

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

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

  if (kind === "video") {
    const video = document.createElement("video");
    preview.classList.add("is-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");
    preview.classList.add("is-image");
    image.src = src;
    image.alt = "";
    image.loading = "lazy";
    image.addEventListener("load", () => analyzeLogoPreview(image, preview, hint, editableField));
    image.addEventListener("error", () => {
      resetMediaPreview(preview, hint);
      preview.textContent = "FILE";
      if (hint) hint.textContent = "Превью не загрузилось. Проверьте путь или внешний URL.";
    });
    preview.append(image);
    return;
  }

  preview.textContent = "FILE";
}

function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, hint, 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, hint, editableField);

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

  markDirty();
}

async function uploadMediaFile({ block, editableField, hiddenInput, urlInput, fileName, preview, hint, error, setSource, file }) {
  if (!file) return;

  state.mediaUploadActive = true;
  setFilePickerActive(true);
  recoverEditorSurface();
  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,
      hint,
      value: payload.url,
    });
    setSource("file");
    markDirty(`Файл сохранён: ${payload.url}. Нажмите «Сохранить модель», чтобы записать путь в content/pages/home.json.`);
  } catch (uploadError) {
    error.textContent = uploadError.message;
    fileName.textContent = truncateText(fileNameFromUrl(hiddenInput.value), 28);
    setStatus(`Ошибка загрузки: ${uploadError.message}`);
  } finally {
    state.mediaUploadActive = false;
    mediaFilePicker.value = "";
    setFilePickerActive(false);
    recoverEditorSurface();
  }
}

function openMediaFilePicker(context) {
  setFilePickerActive(true);
  recoverEditorSurface();
  window.clearTimeout(state.filePickerResetTimer);

  mediaFilePicker.accept = MEDIA_ACCEPT;
  mediaFilePicker.value = "";
  mediaFilePicker.oncancel = () => {
    setFilePickerActive(false);
    recoverEditorSurface();
  };
  mediaFilePicker.onchange = async () => {
    const file = mediaFilePicker.files?.[0];

    if (!file) {
      setFilePickerActive(false);
      recoverEditorSurface();
      return;
    }

    await uploadMediaFile({ ...context, file });
  };

  window.addEventListener(
    "focus",
    () => {
      window.setTimeout(() => {
        if (state.mediaUploadActive || mediaFilePicker.files?.length) return;
        setFilePickerActive(false);
        recoverEditorSurface();
      }, 180);
    },
    { once: true },
  );

  try {
    mediaFilePicker.click();
  } catch (error) {
    setFilePickerActive(false);
    recoverEditorSurface();
    context.error.textContent = error.message;
    setStatus(`Ошибка открытия выбора файла: ${error.message}`);
  }
}

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 fileButton = document.createElement("button");
  const fileName = document.createElement("span");
  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 hint = document.createElement("span");
  const error = document.createElement("span");
  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";
  fileButton.className = "service-media-file-button";
  fileButton.type = "button";
  fileButton.textContent = "Выберите файл";
  fileName.className = "service-media-file-name";
  fileName.textContent = truncateText(fileNameFromUrl(value), 28);
  fileName.title = fileNameFromUrl(value);
  fileControl.append(fileButton, fileName);

  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, hint, editableField);

  path.className = "field-path";
  path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`;
  hint.className = "media-upload-hint";
  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,
      hint,
      value: urlInput.value,
    });
  });

  fileButton.addEventListener("pointerdown", () => {
    setFilePickerActive(true);
    scheduleFilePickerRecovery();
  });
  fileButton.addEventListener("click", () => {
    openMediaFilePicker({
      block,
      editableField,
      hiddenInput,
      urlInput,
      fileName,
      preview,
      hint,
      error,
      setSource,
    });
  });

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

function collectionItems(block, editableField) {
  const value = getByPath(block, editableField.path);
  if (Array.isArray(value)) return value;

  const nextValue = [];
  setByPath(block, editableField.path, nextValue);
  return nextValue;
}

function collectionItemTitle(item, index) {
  return item?.adminLabel || item?.title || item?.name || item?.label || item?.question || item?.windowTitle || `Элемент ${index + 1}`;
}

function collectionItemTemplate(editableField) {
  if (editableField.itemTemplate) return clone(editableField.itemTemplate);

  const item = {};
  for (const itemField of editableField.itemFields ?? []) {
    setByPath(item, itemField.path, itemField.defaultValue ?? "");
  }
  return item;
}

function uniqueCollectionItemId(items, baseId) {
  const root = String(baseId || "item").trim() || "item";
  const ids = new Set(items.map((item) => item?.id).filter(Boolean));
  if (!ids.has(root)) return root;

  let index = 2;
  while (ids.has(`${root}-${index}`)) {
    index += 1;
  }
  return `${root}-${index}`;
}

function prepareCollectionItemForInsert(items, item) {
  const nextItem = clone(item);

  if (Object.prototype.hasOwnProperty.call(nextItem, "id")) {
    nextItem.id = uniqueCollectionItemId(items, nextItem.id);
  }

  return nextItem;
}

function renderCollectionScalarField({ block, field, grid }) {
  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, field.path) ?? "";
  const isLong = field.kind === "html" || field.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 = field.label || field.path;
  kind.className = "field-kind";
  kind.textContent = field.kind || "text";
  path.className = "field-path";
  path.textContent = field.path;
  labelRow.append(labelText, kind);

  input.dataset.path = field.path;
  input.value = value;

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

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

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

function startCollectionPointerDrag(event, card, items, index, rerender) {
  if (event.button !== 0) return;
  event.preventDefault();
  event.stopPropagation();

  const list = card.closest(".collection-list");
  if (!list) return;

  const placeholder = createSortablePlaceholder(card);
  const ghost = createSortableGhost(card, event);

  card.classList.add("dragging", "sorting-source");
  list.classList.add("sorting-active");
  card.after(placeholder);

  const onMove = (moveEvent) => {
    moveSortableGhost(ghost, moveEvent);
    movePlaceholderByPoint({
      event: moveEvent,
      list,
      source: card,
      placeholder,
      selector: ".collection-item-card",
    });
  };

  const onFinish = () => {
    document.removeEventListener("pointermove", onMove);
    document.removeEventListener("pointerup", onFinish);
    document.removeEventListener("pointercancel", onCancel);

    const nextIndex = sortablePlaceholderIndex({
      list,
      placeholder,
      source: card,
      selector: ".collection-item-card",
    });

    cleanupDragState();

    if (nextIndex < 0 || nextIndex === index) return;
    const [moved] = items.splice(index, 1);
    items.splice(Math.min(nextIndex, items.length), 0, moved);
    rerender("Порядок элементов коллекции изменён локально.");
  };

  const onCancel = () => {
    document.removeEventListener("pointermove", onMove);
    document.removeEventListener("pointerup", onFinish);
    document.removeEventListener("pointercancel", onCancel);
    cleanupDragState();
  };

  document.addEventListener("pointermove", onMove);
  document.addEventListener("pointerup", onFinish);
  document.addEventListener("pointercancel", onCancel);
}

function renderCollectionField({ block, editableField, grid }) {
  const items = collectionItems(block, editableField);
  const container = document.createElement("section");
  const head = document.createElement("div");
  const title = document.createElement("div");
  const addButton = document.createElement("button");
  const list = document.createElement("div");

  function rerender(message = null) {
    el.json.value = JSON.stringify(block, null, 2);
    renderContentFields(block);
    markDirty(message);
  }

  container.className = "collection-field";
  head.className = "collection-head";
  title.className = "collection-title";
  title.textContent = editableField.label || editableField.path;
  addButton.className = "collection-add-btn";
  addButton.type = "button";
  addButton.title = `Добавить элемент в «${editableField.label || editableField.path}»`;
  addButton.setAttribute("aria-label", addButton.title);
  addButton.textContent = "+";
  addButton.addEventListener("click", () => {
    items.push(prepareCollectionItemForInsert(items, collectionItemTemplate(editableField)));
    rerender(`Добавлен элемент в «${editableField.label || editableField.path}».`);
  });
  head.append(title, addButton);

  list.className = "collection-list";

  items.forEach((item, index) => {
    const details = document.createElement("details");
    const summary = document.createElement("summary");
    const summaryTitle = document.createElement("span");
    const controls = document.createElement("span");
    const dragHandle = document.createElement("button");
    const body = document.createElement("div");
    const itemGrid = document.createElement("div");

    details.className = "collection-item-card";
    if (index === 0 && items.length <= 8) details.open = true;
    summary.className = "collection-summary";
    summaryTitle.className = "collection-summary-title";
    summaryTitle.textContent = collectionItemTitle(item, index);
    controls.className = "collection-item-actions";
    dragHandle.className = "collection-drag-handle";
    dragHandle.type = "button";
    dragHandle.title = "Перетащить элемент";
    dragHandle.setAttribute("aria-label", "Перетащить элемент");
    dragHandle.textContent = "⋮⋮";
    dragHandle.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
    });
    dragHandle.addEventListener("pointerdown", (event) => startCollectionPointerDrag(event, details, items, index, rerender));
    controls.append(dragHandle);

    [
      ["↑", "Поднять", () => {
        if (index === 0) return;
        const [moved] = items.splice(index, 1);
        items.splice(index - 1, 0, moved);
        rerender("Порядок элементов коллекции изменён локально.");
      }],
      ["↓", "Опустить", () => {
        if (index >= items.length - 1) return;
        const [moved] = items.splice(index, 1);
        items.splice(index + 1, 0, moved);
        rerender("Порядок элементов коллекции изменён локально.");
      }],
      ["⧉", "Дублировать", () => {
        items.splice(index + 1, 0, prepareCollectionItemForInsert(items, item));
        rerender("Элемент коллекции продублирован локально.");
      }],
      ["⌫", "Удалить", () => {
        items.splice(index, 1);
        rerender("Элемент коллекции удалён локально.");
      }],
    ].forEach(([symbol, titleText, handler]) => {
      const button = document.createElement("button");
      button.className = "collection-icon-btn";
      button.type = "button";
      button.title = titleText;
      button.setAttribute("aria-label", titleText);
      button.textContent = symbol;
      button.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        handler();
      });
      controls.append(button);
    });

    summary.append(summaryTitle, controls);
    body.className = "collection-item-body";
    itemGrid.className = "group-grid";

    for (const itemField of editableField.itemFields ?? []) {
      const nestedField = {
        ...itemField,
        path: `${editableField.path}.${index}.${itemField.path}`,
        label: itemField.label || itemField.path,
        group: editableField.group,
      };

      if (nestedField.kind === "media") {
        renderMediaField({ block, editableField: nestedField, grid: itemGrid });
      } else {
        renderCollectionScalarField({ block, field: nestedField, grid: itemGrid });
      }
    }

    body.append(itemGrid);
    details.append(summary, body);
    list.append(details);
  });

  container.append(head, list);
  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 === "collection") {
        renderCollectionField({ block, editableField, grid });
        continue;
      }

      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);
        markDirty();
      });

      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();
    markDirty("JSON блока применён локально. Нажмите «Сохранить модель», чтобы записать файл.");
    return true;
  } catch (error) {
    setStatus(`Ошибка JSON: ${error.message}`);
    return false;
  }
}

async function loadPage() {
  const [pageResponse, templatesResponse] = await Promise.all([
    fetch("/api/page/home"),
    fetch("/api/block-templates/home"),
  ]);

  if (!pageResponse.ok) throw new Error(await pageResponse.text());
  if (!templatesResponse.ok) throw new Error(await templatesResponse.text());

  state.page = await pageResponse.json();
  state.templates = await templatesResponse.json();
  ensureStaticElements();
  state.selected = { kind: "static" };
  renderAll();
  setDirty(false);
  setStatus("Модель home.json загружена.");
}

async function persistPage() {
  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());
}

async function savePage() {
  await persistPage();
  setDirty(false);
  setStatus("Модель сохранена в content/pages/home.json. Для публичной страницы нажмите «Обновить index.html».");
}

async function renderHome() {
  await persistPage();
  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");
  }

  setDirty(false);
  setStatus(payload.stdout ? `Модель сохранена. ${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();
  markDirty("Порядок блоков изменён локально. Нажмите «Сохранить модель» или «Обновить index.html».");
}

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();
  markDirty(`Блок «${duplicate.adminLabel || duplicate.id}» продублирован локально.`);
}

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();
  markDirty(`Блок «${pasted.adminLabel || pasted.id}» вставлен локально.`);
}

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();
  markDirty("Блок удалён локально. Нажмите «Сохранить модель» или «Обновить index.html».");
}

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

el.json.addEventListener("blur", () => {
  if (state.filePickerActive || state.mediaUploadActive) {
    recoverEditorSurface();
    return;
  }

  syncBlockFromJson();
});
el.addBlock.addEventListener("click", openTemplateModal);
el.templateModalClose.addEventListener("click", closeTemplateModal);
el.templateModalCancel.addEventListener("click", closeTemplateModal);
el.templateModal.addEventListener("click", (event) => {
  if (event.target === el.templateModal) closeTemplateModal();
});
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && !el.templateModal.classList.contains("hidden")) {
    closeTemplateModal();
  }
});
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);

window.addEventListener("error", (event) => {
  recoverEditorSurface();
  setStatus(`Ошибка JS: ${event.message}`);
});

window.addEventListener("unhandledrejection", (event) => {
  recoverEditorSurface();
  const reason = event.reason instanceof Error ? event.reason.message : String(event.reason);
  setStatus(`Ошибка JS: ${reason}`);
});

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