import { buildRecipeRowsSkeletonHtml } from "../modules/recipes/recipe-skeleton.js";
import { cowIconSvg, routeIconSvg } from "../wesp-zootech-icons.js";
import {
  peekPreloadedApiPayload,
  clearPreloadedApiPayload,
  clearRecipesBootOverlay,
  PRELOAD_FEED_DISPENSERS_KEY,
  PRELOAD_FEED_DISPENSERS_MAX_AGE_MS,
} from "../wesp-nav-preload.js";

export function createRecipesDataController({
  notyf,
  getSelectedDispenser,
  setSelectedDispenser,
  getSelectedPeriod,
  setSelectedPeriod,
  getSelectedRecipe,
  setSelectedRecipe,
  setCurrentDispenserType,
  getCurrentDispenserType,
  loadRecipeDetails,
  showRecipeEdit,
  updatePasteButtons,
}) {
  /** Сравнение id (UUID/число) без гонок при переключении оборудования/периода */
  function isSameId(a, b) {
    return String(a ?? "") === String(b ?? "");
  }

  function recipeSkipBadgeHtml(label, title) {
    return (
      `<span class="recipe-skip-badge ms-2" title="${title}">` +
      '<span class="recipe-skip-badge__dot" aria-hidden="true"></span>' +
      `${label}</span>`
    );
  }

  function recipeAdjustBadgeHtml(label, title) {
    return (
      `<span class="recipe-adjust-badge ms-2" title="${title}">` +
      '<span class="recipe-row-adjust-dot" aria-hidden="true"></span>' +
      `${label}</span>`
    );
  }

  function recipeSkipRestoreBtn(action, recipeId, title) {
    return (
      `<button type="button" class="btn btn-sm btn-outline-success recipe-skip-restore-btn ms-1" ` +
      `data-action="${action}" data-recipe-id="${recipeId}" ` +
      `title="${title}"><i class="fas fa-undo me-1"></i>Вернуть</button>`
    );
  }

  function dashboardSkipWarningHtml(title = "Сегодня есть отклонения от плана") {
    return (
      `<span class="dashboard-skip-warning ms-2" title="${title}" aria-label="${title}">` +
      '<i class="fas fa-exclamation-triangle" aria-hidden="true"></i></span>'
    );
  }

  function recipeSkipExtrasHtml(recipe) {
    if (recipe.skippedToday) {
      return (
        recipeSkipBadgeHtml("сегодня не кормим", "Сегодня не кормим") +
        recipeSkipRestoreBtn("unskip-recipe-today", recipe.id, "Вернуть рейс в план")
      );
    }
    let html = "";
    if (recipe.skippedIngredientToday) {
      html +=
        recipeSkipBadgeHtml("сегодня убрали ингредиент", "Сегодня убрали ингредиент") +
        recipeSkipRestoreBtn("unskip-ingredient-parts-today", recipe.id, "Вернуть все компоненты в план");
    }
    if (recipe.skippedGroupToday) {
      html +=
        recipeSkipBadgeHtml("сегодня убрали группу", "Сегодня убрали группу") +
        recipeSkipRestoreBtn("unskip-group-parts-today", recipe.id, "Вернуть все группы в план");
    }
    if (recipe.adjustedIngredientToday) {
      html += recipeAdjustBadgeHtml("сегодня изменили норму", "Сегодня изменили норму в плане");
    }
    if (recipe.replacedIngredientToday) {
      html += recipeSkipBadgeHtml("сегодня заменили компонент", "Сегодня заменили компонент в плане");
    }
    return html;
  }

  function recipePlanBadgesRowHtml(recipe) {
    const badges = recipeSkipExtrasHtml(recipe);
    if (!badges) return "";
    return `<div class="recipe-plan-badges">${badges}</div>`;
  }

  /** Верхние 3 списка на десктопе: до 4 карточек без прокрутки; дальше — max-height + scroll */
  const DASHBOARD_MAX_LIST_ITEMS = 4;
  const DASHBOARD_LIST_IDS = new Set(["dispensersList", "periodsList", "recipesList"]);
  /** Минимум времени показа скелетона (мс), чтобы не мигал при очень быстром ответе API */
  const DASHBOARD_SKELETON_MIN_MS = 220;

  /** Длительность FLIP-перестановки карточек (мс) */
  const RECIPES_REORDER_FLIP_MS = 620;

  const MOBILE_DASHBOARD_MQ = "(max-width: 768px)";
  const MOBILE_DASHBOARD_STEPS = ["dispensers", "periods", "recipes"];

  function isMobileDashboardViewport() {
    return globalThis.matchMedia?.(MOBILE_DASHBOARD_MQ)?.matches ?? false;
  }

  function getMobileDashboardStepFromBody() {
    for (const step of MOBILE_DASHBOARD_STEPS) {
      if (document.body.classList.contains(`dashboard-mobile-step-${step}`)) {
        return step;
      }
    }
    return "dispensers";
  }

  function getListItemTitle(listId, dataAttr, id) {
    if (id == null || id === "") return "";
    const item = document.querySelector(`#${listId} .list-item[${dataAttr}="${CSS.escape(String(id))}"]`);
    if (!item) return "";
    const heading = item.querySelector("h4");
    return heading?.textContent?.replace(/\s+/g, " ").trim() ?? "";
  }

  function updateMobileDashboardNav(step) {
    const nav = document.getElementById("recipesMobileNav");
    const titleEl = document.getElementById("recipesMobileNavTitle");
    if (!nav || !titleEl) return;

    if (!isMobileDashboardViewport() || step === "dispensers") {
      nav.hidden = true;
      titleEl.textContent = "";
      return;
    }

    nav.hidden = false;
    const dispenserTitle = getListItemTitle("dispensersList", "data-dispenser-id", getSelectedDispenser());
    const periodTitle = getListItemTitle("periodsList", "data-period-id", getSelectedPeriod());

    if (step === "periods") {
      titleEl.textContent = dispenserTitle || "Периоды кормления";
      return;
    }

    if (getCurrentDispenserType() === "mill") {
      titleEl.textContent = dispenserTitle || "Рецепты";
      return;
    }

    const parts = [dispenserTitle, periodTitle].filter(Boolean);
    titleEl.textContent = parts.length > 0 ? parts.join(" · ") : "Рейсы";
  }

  function setMobileDashboardStep(step) {
    document.body.classList.remove(
      ...MOBILE_DASHBOARD_STEPS.map((s) => `dashboard-mobile-step-${s}`)
    );

    if (!isMobileDashboardViewport()) {
      updateMobileDashboardNav("dispensers");
      return;
    }

    const resolvedStep = MOBILE_DASHBOARD_STEPS.includes(step) ? step : "dispensers";
    document.body.classList.add(`dashboard-mobile-step-${resolvedStep}`);
    updateMobileDashboardNav(resolvedStep);
  }

  function mobileDashboardBack() {
    if (!isMobileDashboardViewport()) return;

    const step = getMobileDashboardStepFromBody();
    if (step === "recipes") {
      showRecipeEdit(false);
      if (getCurrentDispenserType() === "mill") {
        setMobileDashboardStep("dispensers");
      } else {
        setMobileDashboardStep("periods");
      }
      return;
    }

    if (step === "periods") {
      setSelectedDispenser(null);
      setSelectedPeriod(null);
      setMobileDashboardStep("dispensers");
      document.querySelectorAll("#dispensersList .list-item").forEach((item) => {
        item.classList.remove("active");
      });
    }
  }

  function hasMobileDashboardStepClass() {
    return MOBILE_DASHBOARD_STEPS.some((s) =>
      document.body.classList.contains(`dashboard-mobile-step-${s}`)
    );
  }

  function syncMobileDashboardLayout() {
    if (!isMobileDashboardViewport()) {
      document.body.classList.remove(
        ...MOBILE_DASHBOARD_STEPS.map((s) => `dashboard-mobile-step-${s}`)
      );
      updateMobileDashboardNav("dispensers");
      return;
    }

    if (document.body.classList.contains("recipe-edit-mobile-open")) {
      return;
    }

    if (!hasMobileDashboardStepClass()) {
      setMobileDashboardStep("dispensers");
    } else {
      updateMobileDashboardNav(getMobileDashboardStepFromBody());
    }
  }

  globalThis.addEventListener("resize", () => {
    syncMobileDashboardLayout();
    DASHBOARD_LIST_IDS.forEach((listId) => {
      const listEl = document.getElementById(listId);
      if (listEl) scheduleDashboardListLayout(listEl);
    });
    if (!isMobileDashboardViewport()) {
      document.body.classList.remove("recipe-edit-mobile-open");
      const recipeEdit = document.getElementById("recipeEdit");
      if (recipeEdit) {
        recipeEdit.classList.remove("recipe-edit--mobile-active");
      }
      document.documentElement.style.overflow = "";
      document.body.style.overflow = "";
    }
  });

  function clearRecipesListReorderClasses(listEl) {
    if (!listEl) return;
    listEl.classList.remove("recipes-list-reorder-pending", "recipes-list-reorder-settle");
  }

  function captureRecipeCardRects(listEl) {
    const map = new Map();
    if (!listEl) return map;
    listEl.querySelectorAll(":scope > .list-item[data-recipe-id]").forEach((el) => {
      map.set(el.dataset.recipeId, el.getBoundingClientRect());
    });
    return map;
  }

  function playRecipesListReorderFlip(listEl, firstRects) {
    if (!listEl || !firstRects || firstRects.size === 0) {
      clearRecipesListReorderClasses(listEl);
      return;
    }
    const reduceMotion = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
    listEl.classList.remove("recipes-list-reorder-pending");
    if (reduceMotion) {
      clearRecipesListReorderClasses(listEl);
      return;
    }

    const items = [...listEl.querySelectorAll(":scope > .list-item[data-recipe-id]")];
    const toAnimate = [];

    items.forEach((el) => {
      const id = el.dataset.recipeId;
      const first = firstRects.get(id);
      if (!first) return;
      const last = el.getBoundingClientRect();
      const dx = first.left - last.left;
      const dy = first.top - last.top;
      if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return;
      el.style.transition = "none";
      el.style.transformOrigin = "center center";
      el.style.willChange = "transform";
      el.style.boxShadow = "var(--recipe-card-flip-shadow, 0 8px 24px rgba(15, 23, 42, 0.12))";
      el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
      el.style.zIndex = "2";
      el.classList.add("recipe-card-reorder-flip");
      toAnimate.push(el);
    });

    if (toAnimate.length === 0) {
      clearRecipesListReorderClasses(listEl);
      return;
    }

    void listEl.offsetHeight;

    requestAnimationFrame(() => {
      const easing = "cubic-bezier(0.33, 1, 0.68, 1)";
      const sec = RECIPES_REORDER_FLIP_MS / 1000;
      toAnimate.forEach((el) => {
        el.style.transition = `transform ${sec}s ${easing}, box-shadow ${sec}s ${easing}`;
        el.style.transform = "translate3d(0, 0, 0)";
        el.style.boxShadow = "";
      });
    });

    globalThis.setTimeout(() => {
      toAnimate.forEach((el) => {
        el.style.transition = "";
        el.style.transform = "";
        el.style.willChange = "";
        el.style.transformOrigin = "";
        el.style.zIndex = "";
        el.style.boxShadow = "";
        el.classList.remove("recipe-card-reorder-flip");
      });
      clearRecipesListReorderClasses(listEl);
    }, RECIPES_REORDER_FLIP_MS + 100);
  }

  async function awaitDashboardSkeletonMin(skeletonStartedAt, options = {}) {
    if (options.skip) return;
    const elapsed = performance.now() - skeletonStartedAt;
    const rest = DASHBOARD_SKELETON_MIN_MS - elapsed;
    if (rest > 0) {
      await new Promise((resolve) => setTimeout(resolve, rest));
    }
  }

  function getDashboardListScrollCap(listId) {
    if (isMobileDashboardViewport()) return null;
    return DASHBOARD_MAX_LIST_ITEMS;
  }

  function syncDashboardListScrollCap(listEl) {
    if (!listEl?.id || !DASHBOARD_LIST_IDS.has(listEl.id)) return;
    listEl.classList.add("dashboard-list-layout-settling");
    listEl.classList.remove("dashboard-list-scroll-cap");
    listEl.style.removeProperty("max-height");

    const maxItems = getDashboardListScrollCap(listEl.id);
    if (!maxItems) {
      requestAnimationFrame(() => {
        listEl.classList.remove("dashboard-list-layout-settling");
      });
      return;
    }

    const items = listEl.querySelectorAll(":scope > .list-item");
    if (items.length <= maxItems) {
      requestAnimationFrame(() => {
        listEl.classList.remove("dashboard-list-layout-settling");
      });
      return;
    }
    let capPx = 0;
    for (let i = 0; i < maxItems; i++) {
      capPx += items[i].offsetHeight;
      if (i < maxItems - 1) {
        const a = items[i].getBoundingClientRect();
        const b = items[i + 1].getBoundingClientRect();
        capPx += Math.max(0, b.top - a.bottom);
      }
    }
    listEl.style.setProperty("max-height", `${Math.ceil(capPx)}px`, "important");
    listEl.classList.add("dashboard-list-scroll-cap");
    requestAnimationFrame(() => {
      listEl.classList.remove("dashboard-list-layout-settling");
    });
  }

  function ensureDashboardListScrollObservers() {
    if (window._dashboardListScrollObserversReady) return;
    window._dashboardListScrollObserversReady = true;
    if (typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        const listEl = entry.target;
        if (listEl?.id && DASHBOARD_LIST_IDS.has(listEl.id)) {
          syncDashboardListScrollCap(listEl);
        }
      });
    });
    DASHBOARD_LIST_IDS.forEach((listId) => {
      const listEl = document.getElementById(listId);
      if (listEl) observer.observe(listEl);
    });
  }

  function scheduleDashboardListLayout(listEl) {
    if (!listEl?.id || !DASHBOARD_LIST_IDS.has(listEl.id)) return;
    ensureDashboardListScrollObservers();
    const run = () => syncDashboardListScrollCap(listEl);
    requestAnimationFrame(() => {
      requestAnimationFrame(run);
    });
  }

  function prefersReducedMotion() {
    return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
  }

  function getDashboardListSkeletonHtml(listId) {
    const variantByList = {
      dispensersList: "dispensers",
      periodsList: "periods",
      recipesList: "period",
      millRecipesList: "mill",
    };
    return buildRecipeRowsSkeletonHtml(variantByList[listId] || "period");
  }

  function markListLoading(listEl, loading) {
    if (!listEl) return;
    listEl.classList.toggle("list-container--loading", Boolean(loading));
  }

  function setListSkeleton(listEl, listId) {
    if (!listEl) return;
    listEl.innerHTML = getDashboardListSkeletonHtml(listId);
    markListLoading(listEl, true);
    scheduleDashboardListLayout(listEl);
  }

  function setRecipesColumnIdle(idle) {
    const recipesCol = document.getElementById("recipesCol");
    if (recipesCol) {
      recipesCol.classList.toggle("dashboard-col--idle", Boolean(idle));
    }
  }

  function setPeriodsColumnIdle(idle) {
    const periodsCol = document.getElementById("periodsCol");
    if (periodsCol) {
      periodsCol.classList.toggle("dashboard-col--idle", Boolean(idle));
    }
  }

  function fadeOutListSkeleton(listEl) {
    const skel = listEl?.querySelector(
      ":scope > .dashboard-list-skeleton, :scope > .zt-loading-list",
    );
    if (!skel) return;
    skel.remove();
  }

  function replaceDashboardListContent(listEl, html) {
    if (!listEl) return;
    fadeOutListSkeleton(listEl);
    listEl.innerHTML = html;
    markListLoading(listEl, false);
    scheduleDashboardListLayout(listEl);
  }

  function clearStaleDashboardListSkeleton(listEl) {
    if (!listEl?.querySelector(":scope > .dashboard-list-skeleton, :scope > .zt-loading-list")) return;
    listEl.innerHTML = "";
    markListLoading(listEl, false);
    scheduleDashboardListLayout(listEl);
  }

  async function loadDispensers() {
    const dispensersList = document.getElementById("dispensersList");
    let dispensers = peekPreloadedApiPayload(
      PRELOAD_FEED_DISPENSERS_KEY,
      PRELOAD_FEED_DISPENSERS_MAX_AGE_MS,
    );
    const fromPreload = Array.isArray(dispensers);
    const skeletonStartedAt = performance.now();

    if (!fromPreload) {
      setListSkeleton(dispensersList, "dispensersList");
    }

    try {
      if (!fromPreload) {
        const response = await fetch("/api/feed_dispensers");
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        dispensers = await response.json();
      }

      const awaitSkeleton = awaitDashboardSkeletonMin(skeletonStartedAt, { skip: fromPreload });

      const finishList = async (html) => {
        await awaitSkeleton;
        await replaceDashboardListContent(dispensersList, html);
        if (fromPreload) {
          clearPreloadedApiPayload(PRELOAD_FEED_DISPENSERS_KEY);
        }
        clearRecipesBootOverlay();
      };

      if (dispensers.length === 0) {
        await finishList(
          `<div class="alert alert-info">
                            <i class="fas fa-info-circle me-2"></i>
                            Нет доступных кормораздатчиков
                        </div>`,
        );
        setMobileDashboardStep("dispensers");
        return;
      }

      const html = dispensers
        .map((dispenser) => {
          const isMill = dispenser.type === "mill";
          const deviceIcon = isMill ? "fa-industry" : "fa-tractor";
          return `
                    <div class="list-item" data-action="select-dispenser" data-dispenser-id="${dispenser.id}" data-dispenser-type="${dispenser.type || "dispenser"}">
                        <div class="list-item-header">
                            <h4 class="list-item-title">
                                <i class="fas ${deviceIcon} me-2"></i>
                                ${dispenser.name}${dispenser.hasSkipToday ? dashboardSkipWarningHtml(`Отклонения от плана: ${dispenser.name}`) : ""}
                            </h4>
                            ${!isMill ? `<span class="badge bg-primary">${dispenser.periods ? dispenser.periods.length : 0} периодов</span>` : ""}
                        </div>
                        <div class="list-item-meta">
                            <small class="text-muted">
                                <i class="fas fa-user me-1"></i>
                                ${dispenser.operator || "Не указан"}
                            </small>
                            <small class="text-muted">
                                <i class="fas fa-building me-1"></i>
                                ${dispenser.farm || "Не указана"}
                            </small>
                        </div>
                    </div>
                `;
        })
        .join("");

      await finishList(html);
      setMobileDashboardStep("dispensers");
    } catch (error) {
      console.error("Ошибка при загрузке кормораздатчиков:", error);
      clearPreloadedApiPayload(PRELOAD_FEED_DISPENSERS_KEY);
      await awaitDashboardSkeletonMin(skeletonStartedAt);
      await replaceDashboardListContent(
        dispensersList,
        `<div class="alert alert-danger">
                        <i class="fas fa-exclamation-triangle me-2"></i>
                        Ошибка при загрузке кормораздатчиков: ${error.message}
                    </div>`
      );
      clearRecipesBootOverlay();
      notyf.error("Ошибка при загрузке кормораздатчиков");
    }
  }

  async function selectDispenser(dispenserId) {
    setSelectedDispenser(dispenserId);
    setSelectedPeriod(null);
    showRecipeEdit(false, { skipMobileStep: true });

    const dispenserIdStr = String(dispenserId);
    document.querySelectorAll("#dispensersList .list-item").forEach((item) => {
      item.classList.toggle("active", item.dataset.dispenserId === dispenserIdStr);
    });

    const periodsListEl = document.getElementById("periodsList");
    const recipesListEl = document.getElementById("recipesList");
    periodsListEl.innerHTML = "";
    markListLoading(periodsListEl, false);
    recipesListEl.innerHTML = "";
    markListLoading(recipesListEl, false);
    setPeriodsColumnIdle(true);
    setRecipesColumnIdle(true);
    scheduleDashboardListLayout(periodsListEl);
    scheduleDashboardListLayout(recipesListEl);

    const response = await fetch("/api/feed_dispensers");
    const dispensers = await response.json();
    const dispenser = dispensers.find((d) => isSameId(d.id, dispenserId));

    if (!isSameId(getSelectedDispenser(), dispenserId)) {
      return;
    }

    if (isMobileDashboardViewport()) {
      setMobileDashboardStep(dispenser && dispenser.type === "mill" ? "recipes" : "periods");
    }

    const periodsCol = document.getElementById("periodsCol");
    const recipesCol = document.getElementById("recipesCol");
    const recipesHeaderText = document.getElementById("recipesCardHeaderText");
    const recipesCardIcon = document.getElementById("recipesCardIcon");
    const createRecipeBtn = document.getElementById("createRecipeBtn");

    if (dispenser && dispenser.type === "mill") {
      setCurrentDispenserType("mill");
      setPeriodsColumnIdle(true);
      if (periodsCol) {
        periodsCol.style.display = "none";
        periodsCol.classList.remove("col-md-4");
      }
      if (recipesCol) {
        recipesCol.classList.remove("col-md-4");
        recipesCol.classList.add("col-md-8");
      }
      if (recipesHeaderText) recipesHeaderText.textContent = "Рецепты";
      if (recipesCardIcon) {
        recipesCardIcon.innerHTML = '<i class="fas fa-clipboard-list me-2"></i>';
        recipesCardIcon.classList.remove("route-icon");
      }
      if (createRecipeBtn) createRecipeBtn.style.display = "";
      setRecipesColumnIdle(false);
      if (!isSameId(getSelectedDispenser(), dispenserId)) return;
      await loadMillRecipes(dispenserId);
    } else {
      setCurrentDispenserType("dispenser");
      if (createRecipeBtn) createRecipeBtn.style.display = "none";
      setRecipesColumnIdle(true);
      setPeriodsColumnIdle(false);
      if (periodsCol) {
        periodsCol.style.display = "";
        periodsCol.classList.add("col-md-4");
      }
      if (recipesCol) {
        recipesCol.classList.remove("col-md-8");
        recipesCol.classList.add("col-md-4");
      }
      if (recipesHeaderText) recipesHeaderText.textContent = "Рейсы";
      if (recipesCardIcon) {
        recipesCardIcon.innerHTML = routeIconSvg();
        recipesCardIcon.classList.add("route-icon");
        recipesCardIcon.classList.add("me-2");
      }
      if (!isSameId(getSelectedDispenser(), dispenserId)) return;
      await loadPeriods(dispenserId);
    }
  }

  async function loadMillRecipes(dispenserId) {
    const recipesList = document.getElementById("recipesList");
    setListSkeleton(recipesList, "millRecipesList");
    const skeletonStartedAt = performance.now();

    try {
      const response = await fetch(`/api/feed_dispensers/${dispenserId}/recipes`);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const recipes = await response.json();

      if (!isSameId(getSelectedDispenser(), dispenserId)) {
        clearStaleDashboardListSkeleton(recipesList);
        return;
      }

      if (recipes.length === 0) {
        await awaitDashboardSkeletonMin(skeletonStartedAt);
        await replaceDashboardListContent(
          recipesList,
          `<div class="alert alert-info">
                            <i class="fas fa-info-circle me-2"></i>
                            Нет доступных рецептов. Создайте первый рецепт.
                        </div>`
        );
        return;
      }

      const selectedRecipe = getSelectedRecipe();
      await awaitDashboardSkeletonMin(skeletonStartedAt);
      const html = recipes
        .map(
          (recipe) => `
                    <div class="list-item ${selectedRecipe === recipe.id ? "active" : ""}" data-action="select-recipe" data-recipe-id="${recipe.id}">
                        <div class="list-item-header">
                            <h4 class="list-item-title recipe-title">
                                <i class="fas fa-clipboard-list me-2"></i>
                                <span class="recipe-title-text">${recipe.name}</span>
                            </h4>
                            <div class="d-flex align-items-center justify-content-end flex-nowrap recipe-table-row-actions recipe-actions list-item-actions" role="group">
                                <div class="btn-group recipe-table-move-group" role="group">
                                <button type="button"
                                        class="btn btn-outline-secondary btn-sm recipe-table-move-btn"
                                        data-action="copy-recipe"
                                        data-recipe-id="${recipe.id}"
                                        title="Копировать рейс">
                                    <i class="fas fa-copy"></i>
                                </button>
                                </div>
                                <button type="button"
                                        class="btn btn-sm recipe-table-delete-btn"
                                        data-action="delete-recipe"
                                        data-recipe-id="${recipe.id}"
                                        title="Удалить рейс">
                                    <i class="fas fa-trash"></i>
                                </button>
                            </div>
                        </div>
                        ${recipePlanBadgesRowHtml(recipe)}
                        <div class="list-item-meta recipe-card-meta">
                            <small class="text-muted">
                                <span class="cow-icon me-1" aria-hidden="true">${cowIconSvg()}</span>
                                ${recipe.heads_count ?? recipe.headsPerTrip ?? 0} голов
                            </small>
                                        <small class="text-muted">
                                            <i class="fas fa-clock me-1"></i>
                                            ${recipe.mixing_time ?? recipe.mixingTime ?? 0} мин
                                        </small>
                                        <span class="badge bg-primary">
                                            <i class="fas fa-percentage me-1"></i>
                                            ${recipe.trip_percent ?? recipe.tripPercent ?? 100}%
                                        </span>
                        </div>
                    </div>
                `
        )
        .join("");
      await replaceDashboardListContent(recipesList, html);
    } catch (error) {
      console.error("Ошибка при загрузке рецептов кормоцеха:", error);
      if (!isSameId(getSelectedDispenser(), dispenserId)) {
        clearStaleDashboardListSkeleton(recipesList);
        return;
      }
      await awaitDashboardSkeletonMin(skeletonStartedAt);
      await replaceDashboardListContent(
        recipesList,
        `<div class="alert alert-danger">
                        <i class="fas fa-exclamation-triangle me-2"></i>
                        Ошибка при загрузке рецептов: ${error.message}
                    </div>`
      );
      notyf.error("Ошибка при загрузке рецептов");
    }
  }

  async function loadPeriods(dispenserId) {
    const periodsList = document.getElementById("periodsList");
    setPeriodsColumnIdle(false);
    setListSkeleton(periodsList, "periodsList");
    const skeletonStartedAt = performance.now();

    try {
      const response = await fetch(`/api/feed_dispensers/${dispenserId}/periods`);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      const periods = Array.isArray(data) ? data : [];

      if (!isSameId(getSelectedDispenser(), dispenserId)) {
        clearStaleDashboardListSkeleton(periodsList);
        return;
      }

      if (periods.length === 0) {
        await awaitDashboardSkeletonMin(skeletonStartedAt);
        await replaceDashboardListContent(
          periodsList,
          `<div class="alert alert-info">
                            <i class="fas fa-info-circle me-2"></i>
                            Нет доступных периодов
                        </div>`
        );
        return;
      }

      await awaitDashboardSkeletonMin(skeletonStartedAt);
      const html = periods
        .map(
          (period) => `
                    <div class="list-item" data-action="select-period" data-period-id="${period.id}">
                        <div class="list-item-header">
                            <h4 class="list-item-title">
                                <i class="fas fa-calendar me-2"></i>
                                ${period.name}${period.hasSkipToday ? dashboardSkipWarningHtml(`Отклонения от плана: ${period.name}`) : ""}
                            </h4>
                            ${period.recipes && period.recipes.length > 0
                              ? `
                                <span class="badge bg-primary">
                                    <i class="fas fa-check me-1"></i>
                                    ${period.recipes.length} рейсов
                                </span>
                            `
                              : `
                                <span class="badge bg-warning">
                                    <i class="fas fa-exclamation-triangle me-1"></i>
                                    Нет рейсов
                                </span>
                            `}
                        </div>
                        <div class="list-item-meta list-item-meta--actions">
                            <div class="btn-group list-item-actions">
                                <button class="btn btn-sm btn-outline-success paste-recipe-btn" 
                                        data-action="paste-recipe"
                                        data-period-id="${period.id}"
                                        disabled title="Вставить рейс из буфера">
                                    <i class="fas fa-paste me-1"></i>
                                    Вставить
                                </button>
                                ${period.recipes && period.recipes.length > 0
                                  ? `
                                    <button class="btn btn-sm btn-outline-primary" data-action="create-recipe" data-period-id="${period.id}">
                                        <i class="fas fa-plus me-1"></i>
                                        Добавить рейс
                                    </button>
                                `
                                  : `
                                    <button class="btn btn-sm btn-primary" data-action="create-recipe" data-period-id="${period.id}">
                                        <i class="fas fa-plus me-1"></i>
                                        Создать первый рейс
                                    </button>
                                `}
                            </div>
                        </div>
                    </div>
                `
        )
        .join("");

      await replaceDashboardListContent(periodsList, html);
      updatePasteButtons();
    } catch (error) {
      console.error("Ошибка при загрузке периодов:", error);
      if (!isSameId(getSelectedDispenser(), dispenserId)) {
        clearStaleDashboardListSkeleton(periodsList);
        return;
      }
      await awaitDashboardSkeletonMin(skeletonStartedAt);
      await replaceDashboardListContent(
        periodsList,
        `<div class="alert alert-danger">
                        <i class="fas fa-exclamation-triangle me-2"></i>
                        Ошибка при загрузке периодов: ${error.message}
                    </div>`
      );
      notyf.error("Ошибка при загрузке периодов");
    }
  }

  async function selectPeriod(periodId) {
    setSelectedPeriod(periodId);

    document.querySelectorAll("#periodsList .list-item").forEach((item) => {
      item.classList.remove("active");
    });
    document.querySelector(`[data-period-id="${periodId}"]`)?.classList.add("active");
    if (isMobileDashboardViewport()) {
      setMobileDashboardStep("recipes");
    }
    setRecipesColumnIdle(false);
    const recipesListClear = document.getElementById("recipesList");
    recipesListClear.innerHTML = "";
    markListLoading(recipesListClear, false);
    scheduleDashboardListLayout(recipesListClear);

    try {
      await loadRecipes(periodId);
    } catch (error) {
      console.error("Ошибка при загрузке рейсов:", error);
      notyf.error("Ошибка при загрузке рейсов");
    }
  }

  async function loadRecipes(periodId, options = {}) {
    if (!periodId) return;

    const skipRecipesSkeleton = Boolean(options.skipRecipesSkeleton);
    const recipesListPre = document.getElementById("recipesList");
    let skeletonStartedAt = performance.now();
    if (recipesListPre) {
      if (skipRecipesSkeleton) {
        clearRecipesListReorderClasses(recipesListPre);
        recipesListPre.classList.add("recipes-list-reorder-pending");
      } else {
        setListSkeleton(recipesListPre, "recipesList");
        skeletonStartedAt = performance.now();
      }
    }

    try {
      const response = await fetch(`/api/periods/${periodId}/recipes`);
      if (!response.ok) {
        throw new Error("Ошибка при загрузке рецептов");
      }

      const recipes = await response.json();
      const recipesList = document.getElementById("recipesList");
      const selectedRecipe = getSelectedRecipe();

      if (!isSameId(getSelectedPeriod(), periodId)) {
        if (recipesList) clearRecipesListReorderClasses(recipesList);
        if (!skipRecipesSkeleton) clearStaleDashboardListSkeleton(recipesList);
        return;
      }

      if (recipes.length === 0) {
        if (!skipRecipesSkeleton) {
          await awaitDashboardSkeletonMin(skeletonStartedAt);
        } else if (recipesList) {
          clearRecipesListReorderClasses(recipesList);
        }
        const emptyHtml = `
                        <div class="alert alert-info">
                            <i class="fas fa-info-circle me-2"></i>
                            Нет доступных рейсов
                        </div>`;
        if (skipRecipesSkeleton) {
          recipesList.innerHTML = emptyHtml;
          markListLoading(recipesList, false);
          scheduleDashboardListLayout(recipesList);
        } else {
          await replaceDashboardListContent(recipesList, emptyHtml);
        }
        return;
      }

      let firstRects = null;
      if (skipRecipesSkeleton && recipesList) {
        firstRects = captureRecipeCardRects(recipesList);
      }

      if (!skipRecipesSkeleton) {
        await awaitDashboardSkeletonMin(skeletonStartedAt);
      }
      const html = recipes
        .map(
          (recipe, index) => `
                    <div class="list-item ${selectedRecipe === recipe.id ? "active" : ""}" data-action="select-recipe" data-recipe-id="${recipe.id}" data-index="${index}">
                        <div class="list-item-header recipe-actions-row">
                            <div class="list-item-leading">
                                <button type="button"
                                        class="btn btn-outline-secondary btn-sm recipe-drag-handle recipe-table-move-btn"
                                        draggable="true"
                                        data-action="recipe-drag-handle"
                                        title="Потяните ручку вверх или вниз. У края списка — автопрокрутка. Долгое нажатие — перенос."
                                        aria-label="Перетащить рейс: потяните вверх или вниз, у края списка — прокрутка">
                                    <i class="fas fa-grip-vertical" aria-hidden="true"></i>
                                </button>
                            </div>
                            <div class="d-flex align-items-center justify-content-end flex-nowrap recipe-table-row-actions recipe-actions list-item-actions" role="group">
                                <div class="btn-group recipe-table-move-group" role="group">
                                <button type="button"
                                        class="btn btn-outline-secondary btn-sm recipe-table-move-btn"
                                        data-action="move-recipe-up"
                                        data-recipe-id="${recipe.id}"
                                        data-index="${index}"
                                        title="Переместить вверх"
                                        ${index === 0 ? "disabled" : ""}>
                                    <i class="fas fa-arrow-up"></i>
                                </button>
                                <button type="button"
                                        class="btn btn-outline-secondary btn-sm recipe-table-move-btn"
                                        data-action="move-recipe-down"
                                        data-recipe-id="${recipe.id}"
                                        data-index="${index}"
                                        title="Переместить вниз"
                                        ${index === recipes.length - 1 ? "disabled" : ""}>
                                    <i class="fas fa-arrow-down"></i>
                                </button>
                                <button type="button"
                                        class="btn btn-outline-secondary btn-sm recipe-table-move-btn"
                                        data-action="copy-recipe"
                                        data-recipe-id="${recipe.id}"
                                        title="Копировать рейс">
                                    <i class="fas fa-copy"></i>
                                </button>
                                </div>
                                <button type="button"
                                        class="btn btn-sm recipe-table-delete-btn"
                                        data-action="delete-recipe"
                                        data-recipe-id="${recipe.id}"
                                        title="Удалить рейс">
                                    <i class="fas fa-trash"></i>
                                </button>
                            </div>
                        </div>
                        <h4 class="list-item-title recipe-title mb-0">
                            <span class="recipe-title-text">${recipe.name}</span>
                        </h4>
                        ${recipePlanBadgesRowHtml(recipe)}
                        <div class="list-item-meta recipe-card-meta">
                            <small class="text-muted">
                                <span class="cow-icon me-1" aria-hidden="true">${cowIconSvg()}</span>
                                ${recipe.heads_count ?? recipe.headsPerTrip ?? 0} голов
                            </small>
                                        <small class="text-muted">
                                            <i class="fas fa-clock me-1"></i>
                                            ${recipe.mixing_time ?? recipe.mixingTime ?? 0} мин
                                        </small>
                                        <span class="badge bg-primary">
                                            <i class="fas fa-percentage me-1"></i>
                                            ${recipe.trip_percent ?? recipe.tripPercent ?? 100}%
                                        </span>
                        </div>
                    </div>
                `
        )
        .join("");
      if (skipRecipesSkeleton) {
        recipesList.innerHTML = html;
        markListLoading(recipesList, false);
        scheduleDashboardListLayout(recipesList);
      } else {
        await replaceDashboardListContent(recipesList, html);
      }
      if (skipRecipesSkeleton && firstRects && firstRects.size > 0) {
        /* Третий кадр после innerHTML: к этому моменту уже отработал scheduleDashboardListLayout (2× rAF + sync) */
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            requestAnimationFrame(() => playRecipesListReorderFlip(recipesList, firstRects));
          });
        });
      } else if (skipRecipesSkeleton) {
        clearRecipesListReorderClasses(recipesList);
      }
    } catch (error) {
      console.error("Ошибка при загрузке рецептов:", error);
      if (!isSameId(getSelectedPeriod(), periodId)) {
        return;
      }
      if (!skipRecipesSkeleton) {
        await awaitDashboardSkeletonMin(skeletonStartedAt);
      }
      const recipesListErr = document.getElementById("recipesList");
      if (recipesListErr) {
        clearRecipesListReorderClasses(recipesListErr);
        const errHtml = `
                        <div class="alert alert-danger">
                            <i class="fas fa-exclamation-triangle me-2"></i>
                            Ошибка при загрузке рейсов
                        </div>`;
        if (skipRecipesSkeleton) {
          recipesListErr.innerHTML = errHtml;
          markListLoading(recipesListErr, false);
          scheduleDashboardListLayout(recipesListErr);
        } else {
          await replaceDashboardListContent(recipesListErr, errHtml);
        }
      }
      notyf.error("Ошибка при загрузке рецептов");
    }
  }

  async function selectRecipe(recipeId) {
    setSelectedRecipe(recipeId);

    document.querySelectorAll("#recipesList .list-item").forEach((item) => {
      item.classList.remove("active");
    });
    document.querySelector(`[data-recipe-id="${recipeId}"]`)?.classList.add("active");

    const skeletonStartedAt = performance.now();
    showRecipeEdit(true, { loading: true });
    await loadRecipeDetails(recipeId);
    await awaitDashboardSkeletonMin(skeletonStartedAt);
    showRecipeEdit(true, { loading: false });
  }

  return {
    loadDispensers,
    selectDispenser,
    loadMillRecipes,
    loadPeriods,
    selectPeriod,
    loadRecipes,
    selectRecipe,
    getCurrentDispenserType,
    setMobileDashboardStep,
    mobileDashboardBack,
  };
}
