/* ctor-shared.jsx — общие стили и компоненты «Конструктора» */
const { useState: cUseState, useEffect: cUseEffect, useRef: cUseRef } = React;

/* ============ ТОКЕНЫ / СТИЛИ (в стиле калькулятора) ============ */
const ctorStyles = {
  page: {
    maxWidth: 1920, margin: '0 auto', padding: '40px 56px 120px',
    fontFamily: "'Manrope', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
    WebkitFontSmoothing: 'antialiased',
    color: 'var(--foreground)', background: 'transparent', minHeight: '100vh',
    lineHeight: 1.47, letterSpacing: '-.005em', fontSize: 15,
  },
  back: {
    display: 'inline-flex', alignItems: 'center', gap: 6,
    fontSize: 13, fontWeight: 600, color: 'var(--primary)', marginBottom: 16,
    textDecoration: 'none',
  },
  h1: { fontFamily: "'Manrope', system-ui, sans-serif", fontSize: 40, fontWeight: 800, letterSpacing: '-.03em', lineHeight: 1.06, color: 'var(--foreground)', marginBottom: 10 },
  lead: { fontSize: 17, color: 'var(--text-tertiary)', maxWidth: 720, lineHeight: 1.5 },

  /* tabs — glass pill track */
  tabBar: { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 28, marginBottom: 28, padding: 4, background: 'var(--glass-bg)', WebkitBackdropFilter: 'var(--glass-blur)', backdropFilter: 'var(--glass-blur)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-pill)', maxWidth: '100%' },
  tab: {
    display: 'flex', alignItems: 'center', gap: 8, padding: '9px 18px', borderRadius: 'var(--radius-pill)',
    fontSize: 14, fontWeight: 600, color: 'var(--text-secondary)', background: 'transparent', border: 'none',
    cursor: 'pointer', transition: 'all .15s var(--ease-out)', letterSpacing: '-.01em',
  },
  tabActive: { background: 'var(--card)', color: 'var(--foreground)', boxShadow: 'var(--shadow-raised)' },
  tabCount: { fontSize: 11, fontWeight: 700, fontVariantNumeric: 'tabular-nums', padding: '1px 7px', borderRadius: 'var(--radius-pill)', background: 'var(--secondary)', color: 'var(--text-tertiary)' },

  /* segmented — компактный переключатель (глубина иерархии, подвкладки) */
  segmentedTrack: {
    display: 'flex', gap: 2, padding: 3, background: 'var(--secondary)', borderRadius: 10,
    width: 'fit-content', maxWidth: '100%', flexWrap: 'wrap',
  },
  segmentedBtn: (active) => ({
    display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 8,
    border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600,
    transition: 'all .15s var(--ease-out)',
    background: active ? 'var(--card)' : 'transparent',
    color: active ? 'var(--foreground)' : 'var(--text-tertiary)',
    boxShadow: active ? 'var(--shadow-raised)' : 'none',
  }),
  segmentedCount: (active) => ({
    fontSize: 11, fontWeight: 700, fontVariantNumeric: 'tabular-nums', padding: '0 6px', borderRadius: 999,
    background: active ? 'var(--ink)' : 'color-mix(in srgb, var(--text-tertiary) 18%, transparent)',
    color: active ? '#fff' : 'var(--text-secondary)',
  }),

  /* cards — signature glass surface */
  card: {
    background: 'var(--glass-bg-strong)', WebkitBackdropFilter: 'var(--glass-blur)', backdropFilter: 'var(--glass-blur)',
    borderRadius: 'var(--radius-2xl)', padding: '22px 24px',
    border: '1px solid var(--glass-border)', boxShadow: 'var(--shadow-card)',
  },
  sectionLabel: { fontSize: 12, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '.05em' },

  /* buttons */
  btnPrimary: {
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7,
    padding: '11px 18px', borderRadius: 'var(--radius-pill)', background: 'var(--ink)', color: '#fff', border: 'none',
    fontSize: 14, fontWeight: 700, letterSpacing: '-.005em', cursor: 'pointer',
    transition: 'all .15s var(--ease-out)',
  },
  btnGhost: {
    display: 'inline-flex', alignItems: 'center', gap: 6,
    padding: '10px 15px', borderRadius: 'var(--radius-pill)', background: 'var(--glass-bg-strong)', color: 'var(--text-secondary)',
    border: '1px solid var(--glass-border)', fontSize: 13, fontWeight: 600, cursor: 'pointer',
    WebkitBackdropFilter: 'var(--glass-blur)', backdropFilter: 'var(--glass-blur)',
  },
  btnDashed: {
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7,
    width: '100%', padding: '11px', borderRadius: 'var(--radius-sm)', background: 'transparent',
    color: 'var(--primary)', border: '1px dashed #f3c39b', fontSize: 13, fontWeight: 600,
    cursor: 'pointer', transition: 'all .15s',
  },
  iconBtn: {
    width: 30, height: 30, padding: 0, border: 'none', background: 'transparent',
    borderRadius: 8, cursor: 'pointer', display: 'flex', alignItems: 'center',
    justifyContent: 'center', color: 'var(--c-subtle)', transition: 'all .12s', flexShrink: 0,
  },
  iconBtnSm: {
    width: 24, height: 24, padding: 0, border: 'none', background: 'transparent',
    borderRadius: 7, cursor: 'pointer', display: 'flex', alignItems: 'center',
    justifyContent: 'center', color: 'var(--c-subtle)', transition: 'all .12s', flexShrink: 0,
  },

  /* inputs — glass field */
  input: {
    fontFamily: 'inherit', fontSize: 14, color: 'var(--foreground)',
    background: 'var(--input-background)', border: '1px solid var(--c-border)', borderRadius: 'var(--radius-sm)',
    padding: '9px 12px', width: '100%', outline: 'none', transition: 'border-color .15s',
    WebkitBackdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
  },
  fieldLabel: { fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600 },

  /* num field (inline days/pause) */
  numField: { display: 'inline-flex', alignItems: 'center', gap: 4, background: 'var(--input-background)', borderRadius: 8, padding: '3px 8px 3px 4px', border: '1px solid var(--c-border)' },
  numInput: {
    width: 38, border: 'none', background: 'transparent', fontFamily: 'inherit',
    fontSize: 13, fontWeight: 700, color: 'var(--foreground)', textAlign: 'right', outline: 'none',
    fontVariantNumeric: 'tabular-nums', padding: 0, MozAppearance: 'textfield',
  },

  /* dropdown — glass pop */
  dropdown: {
    position: 'absolute', top: 'calc(100% + 4px)', right: 0, zIndex: 50,
    background: 'var(--card)', borderRadius: 'var(--radius-md)', padding: 6, minWidth: 190,
    boxShadow: 'var(--shadow-pop)', border: '1px solid var(--border-subtle)',
  },
  dropdownItem: {
    display: 'flex', alignItems: 'center', gap: 9, width: '100%', textAlign: 'left',
    padding: '9px 10px', borderRadius: 8, border: 'none', background: 'transparent',
    fontSize: 13.5, fontWeight: 600, color: 'var(--foreground)', cursor: 'pointer', fontFamily: 'inherit',
  },

  /* modal */
  modalBackdrop: {
    position: 'fixed', inset: 0, background: 'rgba(20,18,14,.32)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)',
    display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 200, padding: 20,
  },
  modal: { background: 'var(--card)', borderRadius: 'var(--radius-xl)', padding: '26px 28px', width: 'min(440px, 100%)', boxShadow: 'var(--shadow-modal)' },
};

/* ============ InlineInput — редактирование текста по месту ============ */
function InlineInput({ value, onCommit, style, placeholder, multiline, onActivate }) {
  const [local, setLocal] = cUseState(value);
  const [editing, setEditing] = cUseState(false);
  const inRef = cUseRef(null);
  cUseEffect(() => { if (!editing) setLocal(value); }, [value, editing]);
  // Режим "открыть настройки одиночным кликом": input read-only пока не вошли в правку (двойной клик)
  const gated = typeof onActivate === 'function';
  const common = {
    background: 'transparent', border: 'none', outline: 'none',
    padding: '2px 5px', borderRadius: 6, fontFamily: 'inherit', resize: 'none',
    transition: 'background .12s', cursor: (gated && !editing) ? 'pointer' : 'text', ...style,
  };
  const enterEdit = (e) => { e.stopPropagation(); setEditing(true); requestAnimationFrame(() => { if (inRef.current) { inRef.current.focus(); inRef.current.select && inRef.current.select(); } }); };
  const handlers = {
    ref: inRef,
    value: local,
    readOnly: gated && !editing,
    onChange: (e) => setLocal(e.target.value),
    onFocus: (e) => { if (gated && !editing) return; setEditing(true); e.target.style.background = '#fff0e6'; },
    onBlur: (e) => { setEditing(false); e.target.style.background = 'transparent'; if (local !== value) onCommit(local); },
    placeholder,
    title: gated ? 'Клик — открыть настройки · двойной клик — переименовать' : undefined,
    // одиночный клик в gated-режиме открывает настройки; двойной — правит заголовок
    onClick: gated && !editing ? (e) => { e.stopPropagation(); onActivate(); } : undefined,
    onDoubleClick: gated ? enterEdit : undefined,
    onMouseEnter: gated && !editing ? (e) => { e.target.style.background = '#f0ece5'; } : undefined,
    onMouseLeave: gated && !editing ? (e) => { e.target.style.background = 'transparent'; } : undefined,
  };
  if (multiline) {
    return <textarea {...handlers} rows={1} style={common}
      onKeyDown={(e) => { if (e.key === 'Escape') { setLocal(value); setEditing(false); e.currentTarget.blur(); } }} />;
  }
  return <input {...handlers} style={common}
    onKeyDown={(e) => {
      if (e.key === 'Enter') e.currentTarget.blur();
      if (e.key === 'Escape') { setLocal(value); setEditing(false); e.currentTarget.blur(); }
    }} />;
}

/* ============ NumField — число с подписью ============ */
function NumField({ value, onChange, suffix, min = 0, step = 1, title, accent, quietZero }) {
  /* quietZero: значение по умолчанию (0) не должно шуметь в строке — ghost до наведения */
  const quiet = quietZero && !value && !accent;
  return (
    <span style={{
      ...ctorStyles.numField,
      ...(accent ? { background: '#fff0e6', borderColor: '#f3c39b' } : {}),
      ...(quiet ? { background: 'transparent', borderColor: 'transparent', opacity: 0.4 } : {}),
    }} title={title}>
      <input type="number" min={min} step={step} value={value}
        onChange={(e) => onChange(Math.max(min, +e.target.value || 0))}
        style={{ ...ctorStyles.numInput, ...(accent ? { color: '#b5521a' } : {}) }} />
      <span style={{ fontSize: 11, color: accent ? '#e8793a' : '#8a817a', fontWeight: 500, whiteSpace: 'nowrap' }}>{suffix}</span>
    </span>
  );
}

/* ============ RolePill — выбор роли ============ */
function RolePill({ roleId, roles, onChange, size = 'md' }) {
  const [open, setOpen] = cUseState(false);
  const ref = cUseRef(null);
  cUseEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);
  const role = roles.find((r) => r.id === roleId);
  const sm = size === 'sm';
  return (
    <div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
      <button onClick={() => setOpen((o) => !o)} title={role ? role.name : 'Назначить роль'} style={{
        display: 'inline-flex', alignItems: 'center', gap: 5,
        padding: sm ? '2px 7px 2px 3px' : '3px 9px 3px 4px', borderRadius: 'var(--radius-pill, 9999px)',
        border: '.5px solid ' + (role ? role.color + '40' : 'transparent'),
        background: role ? role.color + '14' : 'transparent',
        opacity: role ? 1 : 0.45,
        cursor: 'pointer', fontFamily: 'inherit', transition: 'all .12s', maxWidth: 168,
      }}>
        <span style={{
          width: sm ? 16 : 18, height: sm ? 16 : 18, borderRadius: '50%', flexShrink: 0,
          background: role ? role.color : '#bcb3a7', color: '#fff',
          fontSize: sm ? 8 : 9, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center',
          letterSpacing: 0,
        }}>{role ? role.abbr : '?'}</span>
        <span style={{ fontSize: sm ? 11.5 : 12.5, fontWeight: 500, color: role ? role.color : '#8a817a', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
          {role ? role.name : 'роль'}
        </span>
      </button>
      {open && (
        <div style={{ ...ctorStyles.dropdown, right: 'auto', left: 0, minWidth: 210, maxHeight: 320, overflowY: 'auto' }}>
          <button style={{ ...ctorStyles.dropdownItem, color: '#8a817a' }} onClick={() => { onChange(null); setOpen(false); }}>
            <span style={{ width: 18, height: 18, borderRadius: '50%', background: '#e4ddd2', display: 'inline-block' }}></span>
            Без роли
          </button>
          {roles.map((r) => (
            <button key={r.id} style={{ ...ctorStyles.dropdownItem, ...(r.id === roleId ? { background: '#f0ece5' } : {}) }}
              onClick={() => { onChange(r.id); setOpen(false); }}>
              <span style={{ width: 18, height: 18, borderRadius: '50%', background: r.color, color: '#fff', fontSize: 9, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{r.abbr}</span>
              {r.name}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* ============ InclusionBadge — правило включения задачи ============ */
const INC_META = {
  required:    { label: 'обязательно', color: '#5aad6e', bg: '#f0fdf4' },
  optional:    { label: 'опционально', color: '#8a817a', bg: '#f0ece5' },
  conditional: { label: 'по условию',  color: '#92620a', bg: '#fffbeb' },
};
function InclusionBadge({ inc, cond, dependsOn, onCycle }) {
  const m = INC_META[inc] || INC_META.required;
  const deps = dependsOn || [];
  /* Имя зависимости показывает DepBadge — здесь только статус, чтобы не дублировать чип */
  return (
    <button onClick={onCycle} className={'chip chip--' + (INC_META[inc] ? inc : 'required')}
      title={deps.length ? 'Зависит от: ' + deps.map((d) => d.path || d.nodeId).join(', ') : (cond ? 'Условие: ' + cond : 'Кликните, чтобы сменить')}>
      <span className="chip-dot"></span>
      {m.label}
    </button>
  );
}

/** DepBadge — узел-зависимый (conditional → от кого зависит) */
function DepBadge({ deps, legacyCond, onClick }) {
  if ((!deps || !deps.length) && !legacyCond) return null;
  if (!deps || !deps.length) {
    return (
      <span className="dep-badge dep-badge--legacy" title={'Устаревшее условие: ' + legacyCond}>
        устаревшее условие
      </span>
    );
  }
  const first = deps[0];
  const name = (first.path || first.nodeId || '').split(' › ').pop() || first.nodeId;
  return (
    <button type="button" className="dep-badge" onClick={onClick} title={'Зависит от:\n' + deps.map((d) => d.path || d.nodeId).join('\n')}>
      → {name}{deps.length > 1 ? ` (+${deps.length - 1})` : ''}
    </button>
  );
}

/** UsedByBadge — узел-цель (от кого зависят другие) */
function UsedByBadge({ dependents }) {
  const [open, setOpen] = cUseState(false);
  const ref = cUseRef(null);
  cUseEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);
  if (!dependents || !dependents.length) return null;
  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
      <button type="button" className="usedby-badge" onClick={() => setOpen((o) => !o)} title="Показать зависимые узлы">
        ↳ от {dependents.length}
      </button>
      {open && (
        <div className="usedby-popover">
          <div className="usedby-popover-title">Зависят от этого узла</div>
          {dependents.map((d) => (
            <div key={d.nodeId} className="usedby-popover-row" title={d.path}>
              <span className="usedby-popover-type">{d.nodeType === 'stage' ? 'этап' : d.nodeType === 'work' ? 'подзадача' : 'задача'}</span>
              <span className="usedby-popover-name">{d.name}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ============ KebabMenu — меню действий (•••) ============ */
function KebabMenu({ items, color = '#bcb3a7' }) {
  const [open, setOpen] = cUseState(false);
  const ref = cUseRef(null);
  cUseEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);
  return (
    <div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
      <button onClick={() => setOpen((o) => !o)} style={{ ...ctorStyles.iconBtn, color }} title="Действия">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.7" /><circle cx="12" cy="12" r="1.7" /><circle cx="19" cy="12" r="1.7" /></svg>
      </button>
      {open && (
        <div style={ctorStyles.dropdown}>
          {items.map((it, i) => (
            <button key={i} style={{ ...ctorStyles.dropdownItem, ...(it.danger ? { color: '#f04e62' } : {}) }}
              onClick={() => { setOpen(false); it.onClick(); }}>
              {it.icon}{it.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* свг-иконки helper */
const Icon = {
  trash: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6M14 11v6"></path></svg>,
  copy: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>,
  plus: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>,
  x: <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>,
  chevron: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>,
  box: <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.29 7 12 12 20.71 7"></polyline><line x1="12" y1="22" x2="12" y2="12"></line></svg>,
  briefcase: <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>,
  clipboard: <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="8" y="2" width="8" height="4" rx="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M9 12h6M9 16h6"></path></svg>,
  sliders: <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>,
  star: <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.7 5.8 21 7 14 2 9.3 9 8.5 12 2"></polygon></svg>,
};

Object.assign(window, { ctorStyles, InlineInput, NumField, RolePill, InclusionBadge, DepBadge, UsedByBadge, KebabMenu, Icon, INC_META });
