/* lk-arrange.jsx — перемещение и настройка ОБЫЧНЫХ блоков страниц (LKSection).
   Содержимое секций не переписывается: обвязка надевается снаружи, а зона
   становится 6-колоночным гридом. Порядок задаётся CSS-свойством order,
   ширина — grid-column: span N. React владеет разметкой внутри секции,
   мы трогаем только обёртку — поэтому реконсиляция ни с чем не конфликтует.

   Область (scope) = "<роль>:<зона>". Раскладка живёт в localStorage.
   Цвет и авто-контраст берём из WorkspaceKit — та же логика, что у «Поля». */
(function () {
  const R = React;
  const RD = ReactDOM;
  const { Ic } = window;
  const K = window.WorkspaceKit;
  if (!K) return;
  /* Контекст создаёт тот, кто загрузился первым: в ЛК это lk-common,
     в операционном центре — этот файл (lk-common там не подключён). */
  const Ctx = window.LKArrangeCtx || (window.LKArrangeCtx = R.createContext(null));

  const STORE_KEY = 'lk_arrange_v1';

  /* ── стор: конфиг в localStorage, названия секций — только в памяти ── */
  const listeners = new Set();
  const meta = {};              /* scope -> { id: {title, icon, seq} } */
  let seq = 0;

  const load = () => { try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch (e) { return {}; } };
  let data = load();
  const save = () => { try { localStorage.setItem(STORE_KEY, JSON.stringify(data)); } catch (e) {} };
  const emit = () => listeners.forEach((f) => f());

  const ARR_ROW = 132;
  const arrFixedPx = (fixedH) => (fixedH ? fixedH * ARR_ROW + (fixedH - 1) * 10 : null);
  const scopeData = (scope) => data[scope] || (data[scope] = { order: [], items: {} });
  const itemConf = (scope, id) => scopeData(scope).items[id] || {};

  function register(scope, id, m) {
    meta[scope] = meta[scope] || {};
    if (!meta[scope][id]) meta[scope][id] = { ...m, seq: seq++ };
    const sd = scopeData(scope);
    if (!sd.order.includes(id)) { sd.order.push(id); emit(); }
  }
  function patch(scope, id, p) {
    const sd = scopeData(scope);
    sd.items[id] = { ...sd.items[id], ...p };
    save(); emit();
  }
  function setOrder(scope, order) { scopeData(scope).order = order; save(); emit(); }
  function resetScope(scope) { delete data[scope]; save(); emit(); }
  function resetAll() { data = {}; save(); emit(); }
  /* Полный сброс раскладки страницы, как «Сброс» в операционном центре:
     чистит и порядок/видимость блоков (lk_arrange), и раскладку поля OpsField
     (lk_field_<role>_* без привязки к версии стора / ops_field_*).
     Перезагрузка переинициализирует поле из пресета. */
  function resetLayoutHard() {
    try {
      Object.keys(localStorage)
        .filter((k) => k === STORE_KEY || /^lk_field_/.test(k) || /^ops_field_/.test(k))
        .forEach((k) => localStorage.removeItem(k));
    } catch (e) {}
    data = {}; emit();
    try { location.reload(); } catch (e) {}
  }
  const subscribe = (f) => { listeners.add(f); return () => listeners.delete(f); };

  /* Одна принудительная перерисовка после монтирования: эффекты родителя
     выполняются позже детских, поэтому подписка появляется уже после того,
     как блоки зарегистрировались, и их emit'ы уходят в никуда. */
  const useStore = () => {
    const [, force] = R.useReducer((n) => n + 1, 0);
    R.useEffect(() => { force(); return subscribe(force); }, []);
  };

  /* ── хук зоны: живёт в HubZone, раздаёт контекст секциям ── */
  function useArrangeZone(zoneId) {
    /* ЛК даёт роль через data-lk-role; автономные страницы (опцентр) —
       через data-arrange-scope, потому что роли у них нет. */
    const b = document.body;
    const root = (b && (b.dataset.arrangeScope || b.dataset.lkRole)) || '';
    const scope = root && zoneId ? root + ':' + zoneId : null;
    useStore();
    const [drag, setDrag] = R.useState(null);   /* {id, order} — живой предпросмотр */
    const gridRef = R.useRef(null);

    if (!scope) return null;

    const sd = scopeData(scope);
    const order = (drag && drag.order) || sd.order;
    const orderOf = (id) => { const i = order.indexOf(id); return i < 0 ? 999 : i; };

    /* тащим за ручку; цель — секция под курсором, сторона по её середине.
       Целью может быть только сосед по тому же контейнеру: order переставляет
       элементы лишь среди сиблингов. */
    const beginDrag = (e, id) => {
      e.preventDefault();
      e.stopPropagation();
      try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
      const src = e.currentTarget.closest('.lk-arr');
      const parent = src && src.parentElement;
      const start = [...order];
      setDrag({ id, order: start });
      if (parent) parent.classList.add('is-arr-dragging');

      const clearGapTargets = () => {
        if (!parent) return;
        parent.querySelectorAll(':scope > .ov-wgt-gap.is-drop-target').forEach((g) => g.classList.remove('is-drop-target'));
      };

      const reorderForGap = (ord, gapEl) => {
        if (!parent) return ord;
        const gridIds = [...parent.querySelectorAll(':scope > .lk-arr')]
          .map((el) => el.dataset.secId)
          .filter(Boolean);
        const gridSet = new Set(gridIds);
        if (!gridSet.has(id)) return ord;

        const sub = gridIds.slice().sort((a, b) => ord.indexOf(a) - ord.indexOf(b));
        const without = sub.filter((x) => x !== id);
        const prev = gapEl.previousElementSibling;
        const next = gapEl.nextElementSibling;
        const prevId = prev && prev.classList.contains('lk-arr') ? prev.dataset.secId : null;
        const nextId = next && next.classList.contains('lk-arr') ? next.dataset.secId : null;
        let insertAt = without.length;
        if (prevId != null && gridSet.has(prevId)) {
          const at = without.indexOf(prevId);
          if (at >= 0) insertAt = at + 1;
        } else if (nextId != null && gridSet.has(nextId)) {
          const at = without.indexOf(nextId);
          if (at >= 0) insertAt = at;
        }
        without.splice(insertAt, 0, id);

        let emitted = false;
        const out = [];
        for (const x of ord) {
          if (gridSet.has(x)) {
            if (!emitted) { out.push(...without); emitted = true; }
          } else {
            out.push(x);
          }
        }
        if (!emitted) out.push(...without);
        return out;
      };

      const pickHost = (ev) => {
        const el = document.elementFromPoint(ev.clientX, ev.clientY);
        let host = el && el.closest('.lk-arr');
        if (host && host.parentElement === parent) return host;
        if (!parent) return null;
        let best = null;
        let bestD = Infinity;
        parent.querySelectorAll(':scope > .lk-arr').forEach((h) => {
          const r = h.getBoundingClientRect();
          const cx = r.left + r.width / 2;
          const cy = r.top + r.height / 2;
          const d = (ev.clientX - cx) ** 2 + (ev.clientY - cy) ** 2;
          if (d < bestD) { bestD = d; best = h; }
        });
        return best;
      };

      const onMove = (ev) => {
        clearGapTargets();
        const el = document.elementFromPoint(ev.clientX, ev.clientY);
        const gap = el && el.closest('.ov-wgt-gap');
        if (gap && gap.parentElement === parent) {
          gap.classList.add('is-drop-target');
          setDrag((d) => (d ? { ...d, order: reorderForGap(d.order, gap) } : d));
          return;
        }
        const host = pickHost(ev);
        if (!host) return;
        const overId = host.dataset.secId;
        if (!overId || overId === id) return;
        const r = host.getBoundingClientRect();
        const cx = r.left + r.width / 2;
        const cy = r.top + r.height / 2;
        const dx = ev.clientX - cx;
        const dy = ev.clientY - cy;
        const after = Math.abs(dx) >= Math.abs(dy)
          ? ev.clientX > cx
          : ev.clientY > cy;
        setDrag((d) => {
          if (!d) return d;
          const next = d.order.filter((x) => x !== id);
          const at = next.indexOf(overId);
          if (at < 0) return d;
          next.splice(after ? at + 1 : at, 0, id);
          return { ...d, order: next };
        });
      };
      const onUp = () => {
        clearGapTargets();
        if (parent) parent.classList.remove('is-arr-dragging');
        setDrag((d) => { if (d) setOrder(scope, d.order); return null; });
        window.removeEventListener('pointermove', onMove);
        window.removeEventListener('pointerup', onUp);
      };
      window.addEventListener('pointermove', onMove);
      window.addEventListener('pointerup', onUp);
    };

    return { scope, gridRef, orderOf, dragId: drag && drag.id, beginDrag,
      conf: (id) => itemConf(scope, id),
      patch: (id, p) => patch(scope, id, p),
      register: (id, m) => register(scope, id, m) };
  }

  /* ── хром секции: ручка, развернуть, настройки ── */
  function ArrangeItem({ arrange, id, title, icon, children, noExpand, variant, expand, expandLabel, sizeHint, chartViz }) {
    const conf = arrange.conf(id);
    const [pop, setPop] = R.useState(null);
    const [full, setFull] = R.useState(false);
    const [engaged, setEngaged] = R.useState(false);
    const [cols, setCols] = R.useState(0);
    const popRef = R.useRef(null);
    const hostRef = R.useRef(null);

    /* CSS-свойство order переставляет только сиблингов и только в grid/flex.
       Поэтому секция «сцепляется» с любым таким контейнером — и зоной, и
       вложенной раскладкой вроде .lk-32. Блочный контейнер переводим в
       flex-колонку: маркер data-arr-flow ставится на DOM-узел, которым React
       не управляет, так что реконсиляция его не затирает.
       Ширина доступна в ЛЮБОМ гриде: span считается от реального числа
       колонок родителя, поэтому работает и в 6-колоночной зоне, и в 3-колоночной
       .ov-wgt-grid обзора. Вне грида (flex/block) ширины нет. */
    R.useLayoutEffect(() => {
      const p = hostRef.current && hostRef.current.parentElement;
      if (!p) { setEngaged(false); return; }
      const cs = getComputedStyle(p);
      if (cs.display === 'block') p.dataset.arrFlow = '1';
      const d = getComputedStyle(p).display; /* перечитываем после arrFlow */
      setEngaged(d === 'grid' || d.indexOf('flex') >= 0);
      setCols(d.indexOf('grid') >= 0 ? cs.gridTemplateColumns.split(' ').filter((x) => x && x !== '0px').length : 0);
    });

    R.useEffect(() => { arrange.register(id, { title, icon }); }, [id, title]);
    R.useEffect(() => {
      if (!pop) return;
      const away = (e) => { if (popRef.current && !popRef.current.contains(e.target)) setPop(null); };
      const esc = (e) => { if (e.key === 'Escape') setPop(null); };
      document.addEventListener('mousedown', away); document.addEventListener('keydown', esc);
      return () => { document.removeEventListener('mousedown', away); document.removeEventListener('keydown', esc); };
    }, [pop]);
    R.useEffect(() => {
      if (!full) return;
      const esc = (e) => { if (e.key === 'Escape') setFull(false); };
      document.addEventListener('keydown', esc);
      return () => document.removeEventListener('keydown', esc);
    }, [full]);

    /* скрытая секция не рендерится, но остаётся зарегистрированной —
       иначе её нечем было бы вернуть из панели «Блоки». */
    if (conf.hidden) return null;

    const openPop = (e) => {
      if (pop) { setPop(null); return; }
      const r = e.currentTarget.getBoundingClientRect();
      const hasViz = !!chartViz;
      const width = Math.min(hasViz ? 318 : 300, window.innerWidth - 24);
      const height = hasViz ? 420 : 260;
      setPop({
        left: Math.max(12, Math.min(r.right - width, window.innerWidth - width - 12)),
        top: window.innerHeight - r.bottom >= height + 12 ? r.bottom + 8 : Math.max(12, r.top - height - 8),
      });
    };

    /* ширина = span по реальным колонкам родителя. Инлайн ставим ТОЛЬКО при явном
       выборе — иначе сохраняем родную вёрстку зоны (ov-wgt-wide / data-size). */
    const sizable = cols > 1;
    const fullW = cols || 1;
    const hint = sizeHint === 'full' || sizeHint === 'wide' ? sizeHint : 'std';
    const hintW = hint === 'full' ? fullW : hint === 'wide' ? Math.max(2, Math.round(fullW * 2 / 3)) : Math.max(1, Math.round(fullW / 3));
    const curW = conf.w ? Math.min(conf.w, fullW) : null;
    const pickW = curW || hintW;

    const defaultH = variant === 'wgt' ? 2 : null;
    const pickH = conf.fixedH == null ? defaultH : conf.fixedH;
    const fixedPx = pickH ? arrFixedPx(pickH) : null;

    const color = conf.color || 'default';
    const colorMode = conf.colorMode || null;
    const cv = K.cardVars(color, colorMode);
    /* цвет секции = токены DS переопределяются авто-контрастными значениями,
       поэтому перекрашивается весь контент, а не только фон */
    const style = engaged ? { order: arrange.orderOf(id) } : {};
    if (sizable && curW) style.gridColumn = 'span ' + curW;
    if (fixedPx) {
      style.height = fixedPx;
      style.maxHeight = fixedPx;
      style.overflow = 'hidden';
      style.display = 'flex';
      style.flexDirection = 'column';
      style.minHeight = 0;
    }
    if (cv) {
      Object.assign(style, cv);
      /* всегда синхронизируем DS-токены — иначе outline наследует --card/--foreground родителя */
      if (cv['--ws-fg']) {
        Object.assign(style, {
          '--foreground': cv['--foreground'] || cv['--ws-fg'],
          '--text-secondary': cv['--text-secondary'] || cv['--ws-fg-sub'],
          '--text-tertiary': cv['--text-tertiary'] || cv['--ws-fg-sub'],
          '--border-subtle': cv['--border-subtle'] || cv['--ws-line'],
          '--secondary': cv['--secondary'] || cv['--ws-track'],
        });
      }
      if (cv['--ws-card-bg']) style['--card'] = cv['--card'] || cv['--ws-card-bg'];
    }

    const gripBtn = engaged && (
      <button className="ws-icon-btn lk-arr-grip" title="Перетащить" aria-label={'Переместить «' + title + '»'}
        onPointerDown={(e) => arrange.beginDrag(e, id)}>
        <svg width="14" height="14" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true">
          <circle cx="3" cy="2" r="1.2" /><circle cx="9" cy="2" r="1.2" />
          <circle cx="3" cy="6" r="1.2" /><circle cx="9" cy="6" r="1.2" />
          <circle cx="3" cy="10" r="1.2" /><circle cx="9" cy="10" r="1.2" />
        </svg>
      </button>
    );
    const settingsBtn = (
      <button className="ws-icon-btn" title="Настройки блока" aria-haspopup="dialog" aria-expanded={Boolean(pop)}
        aria-label={'Настройки «' + title + '»'} onClick={openPop}>
        <Ic n="more-horizontal" s={14} />
      </button>
    );
    const expandBtn = expand && (
      <button type="button" className="ws-icon-btn lk-arr-exp" title={expandLabel || 'Открыть полностью'}
        aria-label={expandLabel || 'Открыть полностью'} onClick={expand}>
        <Ic n="arrow-up-right" s={14} />
      </button>
    );
    const maximizeBtn = !noExpand && (
      <button className="ws-icon-btn" title="Развернуть" aria-label={'Развернуть «' + title + '»'} onClick={() => setFull(true)}>
        <Ic n="maximize-2" s={14} />
      </button>
    );
    /* Виджет: ↗ · ⋯ · grip в одной полоске (как в шапке карточки).
       Секция ЛК: grip · maximize · ⋯ — прежний порядок. */
    const chrome = (
      <span className="lk-arr-tools">
        {variant === 'wgt' ? (
          <React.Fragment>{expandBtn}{settingsBtn}{gripBtn}</React.Fragment>
        ) : variant === 'kpi' ? (
          <React.Fragment>{settingsBtn}{gripBtn}</React.Fragment>
        ) : (
          <React.Fragment>{gripBtn}{maximizeBtn}{settingsBtn}</React.Fragment>
        )}
      </span>
    );

    const popover = pop && RD.createPortal((
      <div className={'ws-pop' + (chartViz ? ' ws-pop--viz' : '')} ref={popRef} role="dialog" aria-label={'Настройки «' + title + '»'} style={pop}>
        {sizable && (
          <div className="ws-pop-row">
            <span className="ws-pop-lbl">Ширина</span>
            <div className="ws-colpick">
              <span className="ws-colpick-frac">{pickW}/{fullW}</span>
              <div className="ws-colpick-track" role="group" aria-label="Колонки виджета" style={{ gridTemplateColumns: 'repeat(' + fullW + ', 1fr)' }}>
                {Array.from({ length: fullW }).map((_, i) => (
                  <button
                    key={i}
                    type="button"
                    className={'ws-colpick-cell' + (i < pickW ? ' is-on' : '')}
                    aria-pressed={pickW === i + 1}
                    title={(i + 1) + ' из ' + fullW}
                    aria-label={(i + 1) + ' из ' + fullW}
                    onClick={() => arrange.patch(id, { w: i + 1 })}
                  />
                ))}
              </div>
              <span className="ws-colpick-end">{pickW === fullW ? 'вся' : fullW}</span>
            </div>
            <span className="ws-pop-hint">Сетка зоны: {fullW} колонок — как в поле. Dense заполняет свободные ячейки.</span>
          </div>
        )}
        {(variant === 'wgt' || variant === 'kpi') && (
          <div className="ws-pop-row">
            <span className="ws-pop-lbl">Высота</span>
            {window.FieldKit && window.FieldKit.RowPicker ? (
              <window.FieldKit.RowPicker value={conf.fixedH} max={3} label="Высота блока"
                onChange={(n) => arrange.patch(id, { fixedH: n })} />
            ) : (
              <div className="ws-seg" role="group" aria-label="Высота блока">
                <button type="button" aria-pressed={conf.fixedH == null} onClick={() => arrange.patch(id, { fixedH: null })}>Авто</button>
                {[1, 2, 3].map((n) => (
                  <button key={n} type="button" aria-pressed={conf.fixedH === n} onClick={() => arrange.patch(id, { fixedH: n })}>{n} стр</button>
                ))}
              </div>
            )}
            <span className="ws-pop-hint">1 стр = {ARR_ROW}px. Лишнее уходит в скролл внутри карточки.</span>
          </div>
        )}
        {chartViz && (() => {
          const opts = window.CHART_VIZ_OPTS || [
            { id: 'donut', label: 'Кольцо', hint: 'Доли' },
            { id: 'bar', label: 'Столбцы', hint: 'Сравнение' },
            { id: 'line', label: 'Линия', hint: 'Динамика' },
            { id: 'hbar', label: 'Рейтинг', hint: 'Полосы' },
          ];
          const Glyph = window.ChartVizGlyph;
          const active = conf.viz || chartViz.default || 'hbar';
          const hint = (opts.find((o) => o.id === active) || {}).hint || 'Выберите вид';
          return (
            <div className="ws-pop-row">
              <span className="ws-pop-lbl">Тип графика</span>
              <div className="of-viz" role="radiogroup" aria-label="Тип графика">
                {opts.map((opt) => (
                  <button
                    key={opt.id}
                    type="button"
                    role="radio"
                    className={'of-viz-opt' + (active === opt.id ? ' is-on' : '')}
                    aria-checked={active === opt.id}
                    title={opt.hint}
                    onClick={() => arrange.patch(id, { viz: opt.id })}
                  >
                    {Glyph ? <Glyph id={opt.id} /> : <span className="of-viz-lab">{opt.label}</span>}
                    <span className="of-viz-lab">{opt.label}</span>
                  </button>
                ))}
              </div>
              <span className="ws-pop-hint">{hint}</span>
            </div>
          );
        })()}
        <div className="ws-pop-row">
          <span className="ws-pop-lbl">Цвет</span>
          <K.GroupedPalette
            value={color || 'default'}
            onChange={(cid) => {
              const accent = cid && ['sand', 'linen', 'mint', 'green', 'amber', 'sky'].indexOf(cid) >= 0;
              arrange.patch(id, { color: cid || 'default', colorMode: accent ? 'outline' : null });
            }}
          />
          <span className="ws-pop-hint">Контур карточки, без заливки фона.</span>
        </div>
        <div className="ws-pop-actions">
          {!noExpand && <button className="ws-pop-act" onClick={() => { setPop(null); setFull(true); }}><Ic n="maximize-2" s={14} />Развернуть</button>}
          <button className="ws-pop-act danger" onClick={() => { setPop(null); arrange.patch(id, { hidden: true }); }}>
            <Ic n="eye-off" s={14} />Скрыть блок
          </button>
        </div>
      </div>
    ), document.body);

    /* «развернуть» переносит те же самые дети в модалку — второго экземпляра нет */
    if (full) {
      return RD.createPortal((
        <React.Fragment>
          <div className="ws-modal-ov" onClick={() => setFull(false)} />
          <div className="ws-modal lk-arr-modal" role="dialog" aria-modal="true" aria-label={title}>
            <div className="ws-modal-head">
              <span className="ws-modal-ic"><Ic n={icon || 'square'} s={17} /></span>
              <span className="ws-modal-title">{title}</span>
              <button className="ws-icon-btn" onClick={() => setFull(false)} aria-label="Закрыть"><Ic n="x" s={17} /></button>
            </div>
            <div className="ws-modal-body">{children}</div>
          </div>
        </React.Fragment>
      ), document.body);
    }

    return (
      <div ref={hostRef} className={'lk-arr' + (variant ? ' lk-arr--' + variant : '') + (arrange.dragId === id ? ' is-dragging' : '') + (fixedPx ? ' is-fixed-h' : '')}
        data-sec-id={id} data-color={color} data-color-mode={colorMode || 'fill'} data-size={hint} data-w={sizable && curW ? curW : ''} data-fixed-h={pickH || ''} data-engaged={engaged ? '1' : '0'} style={style}>
        {chrome}
        {popover}
        {children}
      </div>
    );
  }

  /* ── панель: возврат скрытых блоков и сброс раскладки ── */
  function ArrangePanel() {
    useStore();
    const [open, setOpen] = R.useState(false);
    const ref = R.useRef(null);
    R.useEffect(() => {
      if (!open) return;
      const away = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
      document.addEventListener('mousedown', away);
      return () => document.removeEventListener('mousedown', away);
    }, [open]);

    const scopes = Object.keys(meta);
    const rows = [];
    let hiddenCount = 0;
    scopes.forEach((sc) => {
      Object.entries(meta[sc]).forEach(([id, m]) => {
        const c = itemConf(sc, id);
        if (c.hidden) hiddenCount++;
        rows.push({ sc, id, ...m, hidden: !!c.hidden });
      });
    });
    rows.sort((a, b) => a.seq - b.seq);
    if (!rows.length) return null;

    return RD.createPortal((
      <div className="lk-arr-panel">
        {open && (
          <div className="lk-arr-pop" ref={ref}>
            <div className="ws-lib-h">Блоки страницы · глаз скрывает и возвращает</div>
            <div className="lk-arr-list">
              {rows.map((r) => (
                <button key={r.sc + r.id} className={'ws-lib-i' + (r.hidden ? ' is-removed' : '')}
                  onClick={() => patch(r.sc, r.id, { hidden: !r.hidden })}>
                  <Ic n={r.icon || 'square'} s={15} />
                  <span className="lk-arr-name">{r.title}</span>
                  <span className="tag"><Ic n={r.hidden ? 'eye-off' : 'eye'} s={13} />{r.hidden ? 'скрыт' : 'виден'}</span>
                </button>
              ))}
            </div>
            <button className="ws-pop-act danger" onClick={() => { setOpen(false); if (window.confirm('Сбросить раскладку страницы к исходной? Порядок, размеры и виджеты вернутся к стандарту.')) resetLayoutHard(); }}>
              <Ic n="rotate-ccw" s={14} />Сбросить раскладку страницы
            </button>
          </div>
        )}
        <button className="lk-arr-fab" aria-expanded={open} onClick={() => setOpen((v) => !v)}>
          <Ic n="layout-grid" s={15} />Блоки
          {hiddenCount > 0 && <span className="ws-badge">{hiddenCount}</span>}
        </button>
      </div>
    ), document.body);
  }

  Object.assign(window, { useArrangeZone, useArrangeStore: useStore, LKArrangeItem: ArrangeItem, LKArrangePanel: ArrangePanel });
})();
