(function (root, factory) {
  "use strict";

  const api = Object.freeze(factory());
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnMarketNavReorder = api;
})(typeof globalThis === "object" ? globalThis : this, function () {
  "use strict";

  const fixedMarket = "dashboard";
  const originalMarkets = Object.freeze([
    "overseas", "exchange", "index", "kospi", "kosdaq",
    "compare", "theme", "foreign-stock", "expert", "halted"
  ]);
  const originalMarketSet = new Set(originalMarkets);
  const dragMime = "application/x-mbn-market-nav";
  const dragPrefix = "mbn-market-nav:v1:";

  function isOriginalMarket(value) {
    return typeof value === "string" && originalMarketSet.has(value);
  }

  function normalizeOrder(value) {
    if (!Array.isArray(value) || value.length !== originalMarkets.length) return null;
    const seen = new Set();
    for (const market of value) {
      if (!isOriginalMarket(market) || seen.has(market)) return null;
      seen.add(market);
    }
    return Object.freeze([...value]);
  }

  // MainForm.swapTabPages exchanges the source and hovered TabPage. Keep the
  // exact identity-based swap instead of deriving screen meaning from indices.
  function swapOrder(order, sourceMarket, targetMarket) {
    const normalized = normalizeOrder(order);
    if (!normalized || !isOriginalMarket(sourceMarket) ||
        !isOriginalMarket(targetMarket)) return null;
    if (sourceMarket === targetMarket) {
      return Object.freeze({ changed: false, order: normalized });
    }
    const sourceIndex = normalized.indexOf(sourceMarket);
    const targetIndex = normalized.indexOf(targetMarket);
    const next = [...normalized];
    [next[sourceIndex], next[targetIndex]] = [next[targetIndex], next[sourceIndex]];
    return Object.freeze({ changed: true, order: Object.freeze(next) });
  }

  function moveAdjacent(order, sourceMarket, direction) {
    const normalized = normalizeOrder(order);
    if (!normalized || !isOriginalMarket(sourceMarket) ||
        !Number.isInteger(direction) || ![-1, 1].includes(direction)) return null;
    const sourceIndex = normalized.indexOf(sourceMarket);
    const targetIndex = sourceIndex + direction;
    if (targetIndex < 0 || targetIndex >= normalized.length) {
      return Object.freeze({ changed: false, order: normalized });
    }
    return swapOrder(normalized, sourceMarket, normalized[targetIndex]);
  }

  function createDragToken(market) {
    if (!isOriginalMarket(market)) throw new TypeError("A reorderable market is required.");
    return `${dragPrefix}${market}`;
  }

  function parseDragToken(value) {
    if (typeof value !== "string" || !value.startsWith(dragPrefix)) return null;
    const market = value.slice(dragPrefix.length);
    return isOriginalMarket(market) && value === createDragToken(market) ? market : null;
  }

  function directMarketButton(nav, target, includeDashboard = false) {
    if (!nav || !target || typeof target.closest !== "function") return null;
    const button = target.closest("button[data-market]");
    if (!button || button.parentElement !== nav) return null;
    const market = button.dataset?.market;
    if (isOriginalMarket(market) || (includeDashboard && market === fixedMarket)) return button;
    return null;
  }

  function readOrder(nav) {
    if (!nav || !nav.children) return null;
    const buttons = Array.from(nav.children);
    if (buttons.length !== originalMarkets.length + 1) return null;
    const dashboard = directMarketButton(nav, buttons[0], true);
    if (!dashboard || dashboard.dataset.market !== fixedMarket) return null;
    return normalizeOrder(buttons.slice(1).map(button => {
      const direct = directMarketButton(nav, button);
      return direct?.dataset?.market;
    }));
  }

  function applyOrder(nav, order) {
    const normalized = normalizeOrder(order);
    const current = readOrder(nav);
    if (!normalized || !current) return false;
    const byMarket = new Map(Array.from(nav.children).map(button => [
      button.dataset.market,
      button
    ]));
    const dashboard = byMarket.get(fixedMarket);
    if (!dashboard || typeof nav.append !== "function" || typeof nav.prepend !== "function") {
      return false;
    }
    nav.prepend(dashboard);
    for (const market of normalized) nav.append(byMarket.get(market));
    return true;
  }

  function createController(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Market-nav reorder options are required.");
    }
    const nav = options.nav;
    if (!nav || typeof nav.addEventListener !== "function" ||
        typeof nav.removeEventListener !== "function") {
      throw new TypeError("A market nav element is required.");
    }
    const isBlocked = typeof options.isBlocked === "function" ? options.isBlocked : () => false;
    const onReordered = typeof options.onReordered === "function" ? options.onReordered : () => {};
    let mounted = false;
    let draggingMarket = null;

    function blocked() {
      try {
        return isBlocked() === true;
      } catch {
        return true;
      }
    }

    function reorder(result, reason, sourceButton) {
      if (!result?.changed || !applyOrder(nav, result.order)) return false;
      if (typeof sourceButton?.focus === "function") sourceButton.focus();
      onReordered(Object.freeze({
        reason,
        order: result.order,
        activeMarket: Array.from(nav.children)
          .find(button => button.classList?.contains("active"))?.dataset?.market ?? null
      }));
      return true;
    }

    function onDragStart(event) {
      const button = directMarketButton(nav, event.target);
      const market = button?.dataset?.market;
      if (!button || blocked() || !event.dataTransfer) {
        event.preventDefault?.();
        draggingMarket = null;
        return;
      }
      draggingMarket = market;
      button.classList?.add("dragging");
      event.dataTransfer.effectAllowed = "move";
      event.dataTransfer.setData(dragMime, createDragToken(market));
    }

    function onDragOver(event) {
      if (blocked() || !draggingMarket || !directMarketButton(nav, event.target)) return;
      event.preventDefault?.();
      if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
    }

    function clearDragging() {
      draggingMarket = null;
      for (const button of Array.from(nav.children)) button.classList?.remove("dragging");
    }

    function onDrop(event) {
      const target = directMarketButton(nav, event.target);
      if (!target || blocked() || !draggingMarket || !event.dataTransfer) {
        clearDragging();
        return;
      }
      event.preventDefault?.();
      const sourceMarket = parseDragToken(event.dataTransfer.getData(dragMime));
      if (!sourceMarket || sourceMarket !== draggingMarket) {
        clearDragging();
        return;
      }
      const order = readOrder(nav);
      const source = Array.from(nav.children)
        .find(button => button.dataset?.market === sourceMarket);
      const result = swapOrder(order, sourceMarket, target.dataset.market);
      reorder(result, "drag", source);
      clearDragging();
    }

    function onKeyDown(event) {
      if (!event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
          !["ArrowUp", "ArrowDown"].includes(event.key)) return;
      const button = directMarketButton(nav, event.target);
      if (!button) return;
      event.preventDefault?.();
      event.stopPropagation?.();
      if (blocked() || event.repeat) return;
      const direction = event.key === "ArrowUp" ? -1 : 1;
      reorder(
        moveAdjacent(readOrder(nav), button.dataset.market, direction),
        "keyboard",
        button);
    }

    function mount() {
      if (mounted) return true;
      if (!readOrder(nav)) return false;
      for (const button of Array.from(nav.children)) {
        const reorderable = isOriginalMarket(button.dataset?.market);
        button.draggable = reorderable;
        if (reorderable) {
          button.setAttribute?.("aria-keyshortcuts", "Alt+ArrowUp Alt+ArrowDown");
          button.setAttribute?.("title", "드래그 또는 Alt+화살표로 업무 탭 순서 변경");
        }
      }
      nav.addEventListener("dragstart", onDragStart);
      nav.addEventListener("dragover", onDragOver);
      nav.addEventListener("drop", onDrop);
      nav.addEventListener("dragend", clearDragging);
      nav.addEventListener("keydown", onKeyDown);
      mounted = true;
      return true;
    }

    function destroy() {
      if (!mounted) return;
      nav.removeEventListener("dragstart", onDragStart);
      nav.removeEventListener("dragover", onDragOver);
      nav.removeEventListener("drop", onDrop);
      nav.removeEventListener("dragend", clearDragging);
      nav.removeEventListener("keydown", onKeyDown);
      clearDragging();
      mounted = false;
    }

    return Object.freeze({ mount, destroy, readOrder: () => readOrder(nav) });
  }

  return {
    fixedMarket,
    originalMarkets,
    dragMime,
    isOriginalMarket,
    normalizeOrder,
    swapOrder,
    moveAdjacent,
    createDragToken,
    parseDragToken,
    readOrder,
    applyOrder,
    createController
  };
});
