// T2 Falcon Admin — IVR Voice template wizard
// Mirrors the WhatsApp template flow (3 steps + maker/checker lifecycle).
// Shape used inside the wizard's `data.ivr`:
//   {
//     type: 'static' | 'dynamic',
//     nodes: [
//       { id, parent: parentId|null, key: '0'..'9'|'*'|'#'|null, label, segments: [Segment], terminal: TerminalAction|null }
//     ],
//     variables: [ { key, type: 'digit'|'number'|'date'|'time', sample } ],
//   }
//   Segment: { kind: 'recording', name, dur } | { kind: 'variable', key }

const { useState: useStateIvr, useMemo: useMemoIvr, useEffect: useEffectIvr, useRef: useRefIvr, useLayoutEffect: useLayoutEffectIvr } = React;

// ------------------------------ Defaults / utils ------------------------------

const ivrUid = () => 'n_' + Math.random().toString(36).slice(2, 9);

const defaultIvrData = (type) => ({
  type: type || 'static',
  nodes: [],
  variables: [],
});

// Sample variables offered in the dynamic "Variable" dropdown out of the box (the user
// can still add their own via the "+" button). One per supported type.
const IVR_DEFAULT_VARS = [
  { key: 'customer_name',    type: 'string', sample: 'Ahmed Ali' },
  { key: 'account_balance',  type: 'number', sample: '1,500.50' },
  { key: 'otp_code',         type: 'digit',  sample: '123456' },
  { key: 'appointment_date', type: 'date',   sample: '2026-03-05' },
  { key: 'appointment_time', type: 'time',   sample: '14:30' },
];
const ivrVarTypeLabel = (type) => ({ number: 'Number', digit: 'Digit', date: 'Date', time: 'Time', string: 'String', name: 'String', datetime: 'Date' }[type] || type || '');

// Build a parent → children map from a flat node list
const buildIvrTree = (nodes) => {
  const byParent = {};
  nodes.forEach(n => {
    const p = n.parent || '__root__';
    (byParent[p] = byParent[p] || []).push(n);
  });
  Object.values(byParent).forEach(list => list.sort((a, b) => (a.key || '').localeCompare(b.key || '')));
  return byParent;
};

// Estimate playback duration (recordings sum + 0.5s per variable read)
// A node's DTMF keys (supports multi-select). Falls back to the legacy single `key`.
const ivrNodeKeys = (n) => (n && Array.isArray(n.keys) && n.keys.length) ? n.keys : (n && n.key ? [n.key] : []);
// Compact key list for an edge label — keeps "Press N" pills small even when a node
// is reachable by many keys (show the first few, then "+N more" as a count).
const ivrKeysLabel = (keys) => (keys.length > 5) ? `${keys.slice(0, 4).join(' / ')} +${keys.length - 4}` : keys.join(' / ');

const estimateNodeDuration = (node) => {
  let s = 0;
  (node.segments || []).forEach(seg => {
    if (seg.kind === 'recording') s += (seg.dur || 5);
    else if (seg.kind === 'variable') s += 1.2;
  });
  return s;
};

// ------------------------------ Type picker ------------------------------

const IvrTypeCard = ({ id, title, desc, icon, selected, onSelect }) => (
  <button type="button" className={`ivr-type-card ${selected ? 'selected' : ''}`} onClick={() => onSelect(id)}>
    <span className="ivr-type-icon">{icon}</span>
    <span className="ivr-type-title">{title}</span>
    <span className="ivr-type-desc">{desc}</span>
    {selected && <span className="ivr-type-check"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>}
  </button>
);

const IcIvrStatic = (
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
    <circle cx="12" cy="5" r="2"/><circle cx="6" cy="17" r="2"/><circle cx="18" cy="17" r="2"/>
    <path d="M12 7v4"/><path d="M12 11l-6 4"/><path d="M12 11l6 4"/>
  </svg>
);
const IcIvrDynamic = (
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
    <circle cx="12" cy="5" r="2"/><circle cx="5" cy="17" r="2"/><circle cx="12" cy="17" r="2"/><circle cx="19" cy="17" r="2"/>
    <path d="M12 7v4"/><path d="M12 11l-7 4"/><path d="M12 11v4"/><path d="M12 11l7 4"/>
  </svg>
);

// ------------------------------ Step 1: Basics + Type ------------------------------

const TplIvrStep1 = ({ data, setData, t }) => {
  const ivr = data.ivr || defaultIvrData('static');
  const setType = (type) => setData({ ...data, ivr: { ...ivr, type } });
  return (
    <div>
      <div className="ivr-step1-grid">
        <div className="tpl-field ivr-f-name">
          <label className="tpl-field-label">{t.templateNameLbl}</label>
          <div className="tpl-input-with-counter">
            <input className="tpl-field-input" placeholder={t.templateNamePh} value={data.name || ''} onChange={(e) => setData({ ...data, name: e.target.value.slice(0, 512) })} />
            <span className="tpl-input-counter">{(data.name || '').length}/512</span>
          </div>
        </div>
        <div className="tpl-field ivr-f-ref">
          <label className="tpl-field-label">{t.referenceIdLbl}</label>
          <input className="tpl-field-input" placeholder={t.referenceIdPh || ''} value={data.referenceId || ''} onChange={(e) => setData({ ...data, referenceId: e.target.value })} />
        </div>
      </div>

      <div className="ivr-section-head">
        <h3>{t.ivrChooseType || 'Choose IVR type'}</h3>
        <p>{t.ivrChooseTypeDesc || 'Static plays one recording per node. Dynamic mixes recordings with resolvable variables.'}</p>
      </div>

      <div className="ivr-type-grid">
        <IvrTypeCard
          id="static"
          icon={IcIvrStatic}
          title={t.ivrTypeStatic || 'Static IVR Tree'}
          desc={t.ivrTypeStaticDesc || 'Each node contains one voice recording. Played according to hierarchy and recipient selection.'}
          selected={ivr.type === 'static'}
          onSelect={setType}
        />
        <IvrTypeCard
          id="dynamic"
          icon={IcIvrDynamic}
          title={t.ivrTypeDynamic || 'Dynamic IVR Tree'}
          desc={t.ivrTypeDynamicDesc || 'Each node may combine recordings and variables (Digit, Number, Date, Time) resolved per recipient.'}
          selected={ivr.type === 'dynamic'}
          onSelect={setType}
        />
      </div>
    </div>
  );
};

// ------------------------------ Step 2: Tree builder ------------------------------

const IcUpload   = <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>;
const IcPlusSm   = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>;
const IvrIcTrash    = <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>;
const IcChev     = <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 6 15 12 9 18"/></svg>;
const IcChevDownTree  = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>;
const IcChevRightTree = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 6 15 12 9 18"/></svg>;
const IcTree     = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.85" strokeLinecap="round" strokeLinejoin="round"><rect x="3.5" y="3.5" width="8" height="5.5" rx="1.3"/><rect x="14" y="9.5" width="6.5" height="4.5" rx="1.2"/><rect x="14" y="16.5" width="6.5" height="4.5" rx="1.2"/><path d="M7.5 9v8.75h6.5M7.5 11.75h6.5"/></svg>;
const IcZoomIn   = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>;
const IcZoomOut  = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>;
const IvrIcClose    = <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>;
const IcPlusBig  = <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>;
const IcVoice    = <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>;
const IcReset    = <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>;
const IcPlay     = <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"><circle cx="12" cy="12" r="9"/><path d="M10 8.2l6 3.8-6 3.8z" fill="currentColor" stroke="none"/></svg>;
const IcPause    = <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"><circle cx="12" cy="12" r="9"/><line x1="10" y1="8.6" x2="10" y2="15.4" strokeWidth="2"/><line x1="14" y1="8.6" x2="14" y2="15.4" strokeWidth="2"/></svg>;
const IcPlaySm   = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><circle cx="12" cy="12" r="9"/><path d="M10 8.4l6 3.6-6 3.6z" fill="currentColor" stroke="none"/></svg>;
const IcPauseSm  = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><circle cx="12" cy="12" r="9"/><line x1="10" y1="8.8" x2="10" y2="15.2" strokeWidth="2"/><line x1="14" y1="8.8" x2="14" y2="15.2" strokeWidth="2"/></svg>;
const IcStopSm   = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><circle cx="12" cy="12" r="9"/><rect x="8.6" y="8.6" width="6.8" height="6.8" rx="1.4" fill="currentColor" stroke="none"/></svg>;
// "Unfold" (chevrons apart) = expand all · "Fold" (chevrons together) = collapse all
const IcExpandAll = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="7 10 12 5 17 10"/><polyline points="7 14 12 19 17 14"/></svg>;
const IcCollapseAll = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="7 6 12 11 17 6"/><polyline points="7 18 12 13 17 18"/></svg>;
const IcJump = <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>;
const IvrIcMore     = <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="12" cy="5" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="12" cy="19" r="1.7"/></svg>;
const IvrIcCenter   = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3.2"/><line x1="12" y1="2.5" x2="12" y2="5.5"/><line x1="12" y1="18.5" x2="12" y2="21.5"/><line x1="2.5" y1="12" x2="5.5" y2="12"/><line x1="18.5" y1="12" x2="21.5" y2="12"/></svg>;
const IvrIcExpand   = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M16 3h3a2 2 0 0 1 2 2v3"/><path d="M8 21H5a2 2 0 0 1-2-2v-3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>;
const IvrIcCompress = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 8h3a2 2 0 0 0 2-2V3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M21 16h-3a2 2 0 0 0-2 2v3"/></svg>;

// Tree row (recursive)
const IvrTreeRow = ({ nodeId, byParent, nodesMap, selectedId, onSelect, onAddChild, onDelete, depth = 0 }) => {
  const node = nodesMap[nodeId];
  if (!node) return null;
  const kids = byParent[nodeId] || [];
  return (
    <div className="ivr-tree-row-wrap" style={{ '--depth': depth }}>
      <div className={`ivr-tree-row ${selectedId === nodeId ? 'selected' : ''}`} onClick={() => onSelect(nodeId)}>
        <span className="ivr-tree-twist">{kids.length > 0 ? IcChev : <span className="dot" />}</span>
        {node.key && <span className="ivr-tree-key">{node.key}</span>}
        <span className="ivr-tree-label">{node.label || 'Untitled'}</span>
        <span className="ivr-tree-meta">{(node.segments || []).length} seg</span>
        <span className="ivr-tree-actions" onClick={(e) => e.stopPropagation()}>
          <button type="button" className="ivr-icon-btn" title="Add child" onClick={() => onAddChild(nodeId)}>{IcPlusSm}</button>
          {node.parent && (
            <button type="button" className="ivr-icon-btn danger" title="Delete" onClick={() => onDelete(nodeId)}>{IvrIcTrash}</button>
          )}
        </span>
      </div>
      {kids.map(k => (
        <IvrTreeRow key={k.id} nodeId={k.id} byParent={byParent} nodesMap={nodesMap} selectedId={selectedId} onSelect={onSelect} onAddChild={onAddChild} onDelete={onDelete} depth={depth + 1} />
      ))}
    </div>
  );
};

// Segment editor for the selected node
const IvrSegmentEditor = ({ node, ivr, onChangeSegments, onAddRecording, onAddVariable, onRemoveSegment, t }) => {
  if (!node) return null;
  return (
    <div className="ivr-seg-list">
      {(node.segments || []).length === 0 && (
        <div className="ivr-empty">{t.ivrEmptySegments || 'No segments yet. Add a recording to get started.'}</div>
      )}
      {(node.segments || []).map((seg, idx) => (
        <div key={idx} className={`ivr-seg ivr-seg-${seg.kind}`}>
          <span className="ivr-seg-idx">{idx + 1}</span>
          {seg.kind === 'recording' ? (
            <>
              <span className="ivr-seg-ic">{IcUpload}</span>
              <span className="ivr-seg-name">{seg.name || 'recording.wav'}</span>
              <span className="ivr-seg-dur">{seg.dur ? seg.dur + 's' : ''}</span>
            </>
          ) : (
            <>
              <span className="ivr-seg-var-pill">{`{${seg.key}}`}</span>
              <span className="ivr-seg-name">{(ivr.variables.find(v => v.key === seg.key) || {}).type || 'variable'}</span>
            </>
          )}
          <button type="button" className="ivr-icon-btn danger" title="Remove" onClick={() => onRemoveSegment(idx)}>{IvrIcTrash}</button>
        </div>
      ))}
      <div className="ivr-seg-add-row">
        <button type="button" className="btn btn-secondary ivr-add-btn" onClick={onAddRecording}>
          {IcUpload} {t.ivrAddRecording || 'Add recording'}
        </button>
        {ivr.type === 'dynamic' && (
          <button type="button" className="btn btn-secondary ivr-add-btn" onClick={onAddVariable}>
            {IcPlusSm} {t.ivrAddVariable || 'Add variable'}
          </button>
        )}
      </div>
    </div>
  );
};

// Voice picker — a Falcon-styled dropdown whose list is split into two source
// tabs: "Uploaded by me" (the client's own recordings + uploads) and "Shared
// with me" (the shared voice library), so you can tell where each voice comes from.
const IcChevDown = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>;
const IcCheckSm  = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>;

const IvrVoicePicker = ({ value, onChange, myVoices, sharedVoices, t }) => {
  const [open, setOpen] = useStateIvr(false);
  const [tab, setTab] = useStateIvr('mine');
  const rootRef = useRefIvr(null);
  useEffectIvr(() => {
    if (!open) return;
    const onDoc = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  const list = tab === 'mine' ? (myVoices || []) : (sharedVoices || []);
  return (
    <div className="ivr-voice-picker" ref={rootRef}>
      <button type="button" className={`ivr-voice-trigger ${open ? 'is-open' : ''}`} onClick={() => setOpen(o => !o)}>
        <span className={`ivr-voice-trigger-val ${value ? '' : 'is-ph'}`}>{value || (t.selectPlaceholder || '-Select-')}</span>
        <span className="ivr-voice-caret">{IcChevDown}</span>
      </button>
      {open && (
        <div className="ivr-voice-menu">
          <div className="ivr-voice-tabs">
            <button type="button" className={`ivr-voice-tab ${tab === 'mine' ? 'active' : ''}`} onClick={() => setTab('mine')}>{t.ivrVoiceMine || 'Uploaded by me'}</button>
            <button type="button" className={`ivr-voice-tab ${tab === 'shared' ? 'active' : ''}`} onClick={() => setTab('shared')}>{t.ivrVoiceShared || 'Shared with me'}</button>
          </div>
          <div className="ivr-voice-options">
            {list.length === 0 && <div className="ivr-voice-none">{t.ivrVoiceNone || 'Nothing here yet.'}</div>}
            {list.map(v => (
              <button type="button" key={v} className={`ivr-voice-option ${value === v ? 'is-selected' : ''}`} onClick={() => { onChange(v); setOpen(false); }}>
                <span className="ivr-voice-option-ic">{IcVoice}</span>
                <span className="ivr-voice-option-name">{v}</span>
                {value === v && <span className="ivr-voice-option-check">{IcCheckSm}</span>}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// Falcon-themed dropdown for a simple option list — same chrome as the voice picker
// (custom trigger + white card menu), so every IVR dropdown matches the theme.
// options: [{ value, label }].
const IvrSelect = ({ value, onChange, options, placeholder, onAddNew, addNewLabel, emptyLabel, t }) => {
  const [open, setOpen] = useStateIvr(false);
  const rootRef = useRefIvr(null);
  useEffectIvr(() => {
    if (!open) return;
    const onDoc = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  const sel = (options || []).find(o => o.value === value);
  return (
    <div className="ivr-voice-picker ivr-select" ref={rootRef}>
      <button type="button" className={`ivr-voice-trigger ${open ? 'is-open' : ''}`} onClick={() => setOpen(o => !o)}>
        <span className={`ivr-voice-trigger-val ${sel ? '' : 'is-ph'}`}>{sel ? sel.label : (placeholder || (t.selectPlaceholder || '-Select-'))}</span>
        <span className="ivr-voice-caret">{IcChevDown}</span>
      </button>
      {open && (
        <div className="ivr-voice-menu ivr-select-menu">
          <div className="ivr-voice-options">
            {(options || []).length === 0 && <div className="ivr-voice-none">{emptyLabel || t.ivrNoOptions || 'Nothing here yet.'}</div>}
            {(options || []).map(o => (
              <button type="button" key={o.value} className={`ivr-voice-option ${value === o.value ? 'is-selected' : ''}`} onClick={() => { onChange(o.value); setOpen(false); }}>
                <span className="ivr-voice-option-name">{o.label}</span>
                {o.hint && <span className="ivr-select-opt-hint">{o.hint}</span>}
                {value === o.value && <span className="ivr-voice-option-check">{IcCheckSm}</span>}
              </button>
            ))}
          </div>
          {onAddNew && (
            <button type="button" className="ivr-select-addnew" onClick={() => { setOpen(false); onAddNew(); }}>
              {IcPlusSm} {addNewLabel || (t.ivrAddNewVar || 'Add new')}
            </button>
          )}
        </div>
      )}
    </div>
  );
};

// `inspect`: in a read-only canvas (template details), still let the user CLICK a node to
// open its side panel and view/tweak each value (defaults) — but keep add/delete hidden.
const TplIvrStep2 = ({ data, setData, t, readOnly = false, inspect = false }) => {
  const ivr = data.ivr || defaultIvrData('static');
  // Read-only views (More Details / Share / Pending review): clicking a node opens the panel
  // ONLY to edit its Variables — and only for a dynamic flow (a static node has no variables).
  const canOpenNode = !readOnly || (inspect && ivr.type === 'dynamic');
  // Empty canvas to start. The canvas "+" creates the ENTRY ("Voice 1"); each
  // node's bottom dot / in-card "+" adds a CHILD ("Voice 1.1") reached by a DTMF
  // key, drawn with a curved "Press N" connector.
  const updateIvr = (patch) => setData({ ...data, ivr: { ...ivr, ...patch } });

  const [zoom, setZoom] = useStateIvr(1);
  const [pan, setPan] = useStateIvr({ x: 0, y: 0 });   // canvas offset (drag to move, like a map)
  const [panning, setPanning] = useStateIvr(false);
  const [fullscreen, setFullscreen] = useStateIvr(false);   // canvas blown up to fill the viewport
  const dragRef = useRefIvr({ active: false, sx: 0, sy: 0, px: 0, py: 0 });
  const [panelOpen, setPanelOpen] = useStateIvr(false);
  const [panelTab, setPanelTab] = useStateIvr('node');   // edit panel: 'node' form | 'variables' values
  const [editId, setEditId] = useStateIvr(null);
  const [parentForNew, setParentForNew] = useStateIvr(null); // parent id when ADDING a child (null = entry)
  const [selectedId, setSelectedId] = useStateIvr(null);     // highlighted node; the stage "+" adds a child under it
  const [draft, setDraft] = useStateIvr({ name: '', voice: '', timeout: '', varType: '', varName: '', keys: [], kind: 'voice', segments: [] });
  const [draftPos, setDraftPos] = useStateIvr({ x: 80, y: 80 });
  // Voice sources, split for the picker's two tabs.
  const [myVoices, setMyVoices] = useStateIvr(['sound name 1', 'IVR-welcome.wav', 'Main-menu-AR.mp3']);
  const sharedVoices = ['Layla — Female (AR)', 'Omar — Male (AR)', 'Sara — Female (EN)'];
  const [play, setPlay] = useStateIvr(null); // { nodeId, idx, paused } — sequences a node's voice segments; pause freezes the progress bar at its position and resumes from there
  const scrollRef = useRefIvr(null);
  const fileRef = useRefIvr(null);
  const [varModalOpen, setVarModalOpen] = useStateIvr(false);   // "add variable" modal (dynamic)
  const [varModalSegIdx, setVarModalSegIdx] = useStateIvr(null); // which dynamic segment the var modal targets
  const uploadSegIdxRef = useRefIvr(null);                       // which dynamic segment an upload targets
  const [confirmDel, setConfirmDel] = useStateIvr(null);          // node id pending delete confirmation
  const [nodeHeights, setNodeHeights] = useStateIvr({});          // measured card heights so connectors meet variable-height cards

  // ----- Resizable canvas | panel split (drag the divider — same as Wallet & Balance) -----
  // Panel width lives in state (null → CSS default 340px). Dragging the divider LEFT
  // widens the panel so the caller can inspect the construction more clearly; the
  // canvas keeps the rest. Double-click resets to default.
  const stepRef = useRefIvr(null);
  const [panelW, setPanelW] = useStateIvr(null);
  const [resizing, setResizing] = useStateIvr(false);
  // Tree (outline) view — toggled from the stage; collapse state per node.
  const [treeView, setTreeView] = useStateIvr(false);
  const [treeCollapsed, setTreeCollapsed] = useStateIvr({});
  const PANEL_MIN = 300, CANVAS_MIN = 280;
  const panelMaxW = () => { const c = stepRef.current; return c ? Math.max(PANEL_MIN, c.getBoundingClientRect().width - CANVAS_MIN - 16) : 720; };
  const startPanelResize = (startEv) => {
    startEv.preventDefault();
    const c = stepRef.current; if (!c) return;
    const rect = c.getBoundingClientRect();
    setResizing(true);
    document.body.style.userSelect = 'none';
    document.body.style.cursor = 'col-resize';
    const onMove = (ev) => {
      if (ev.cancelable) ev.preventDefault();
      const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
      const max = Math.max(PANEL_MIN, rect.width - CANVAS_MIN - 16);
      setPanelW(Math.round(Math.min(max, Math.max(PANEL_MIN, rect.right - clientX))));
    };
    const stop = () => {
      setResizing(false);
      document.body.style.userSelect = '';
      document.body.style.cursor = '';
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', stop);
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', stop);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', stop);
    document.addEventListener('touchmove', onMove, { passive: false });
    document.addEventListener('touchend', stop);
  };
  const onPanelResizeKey = (e) => {
    if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
    e.preventDefault();
    const base = panelW || 340;
    const next = e.key === 'ArrowLeft' ? base + 24 : base - 24;   // ◄ widens the panel (divider moves left)
    setPanelW(Math.round(Math.min(panelMaxW(), Math.max(PANEL_MIN, next))));
  };

  const NODE_W = 260, NODE_H = 112, V_GAP = 74, H_GAP = 40;
  const KEYS = ['1','2','3','4','5','6','7','8','9','0','*','#'];
  const fmtDur = (s) => `00:${String(Math.max(0, Math.round(s || 0))).padStart(2, '0')}s`;

  // Canvas position for every node — a TIDY TREE layout so cards never stack on top of one
  // another. Each subtree gets its own horizontal band: siblings (with their whole subtrees)
  // are laid out left-to-right by subtree width, and every parent is centred over its children.
  // Y flows top-down from each parent's MEASURED bottom, so a card that grows (more segments)
  // pushes its children DOWN and the parent→child connectors stay correctly shaped.
  const nodes = ivr.nodes.map(n => ({ ...n, _x: 0, _y: 0 }));
  (() => {
    const kidsOf = (pid) => nodes.filter(n => (n.parent || null) === pid);
    const wCache = {};
    const subtreeW = (n) => {
      if (wCache[n.id] != null) return wCache[n.id];
      const kids = kidsOf(n.id);
      let w = NODE_W;
      if (kids.length) w = Math.max(NODE_W, kids.reduce((s, c) => s + subtreeW(c), 0) + H_GAP * (kids.length - 1));
      wCache[n.id] = w;
      return w;
    };
    const placeX = (n, leftX) => {
      const kids = kidsOf(n.id);
      if (!kids.length) { n._x = leftX; return; }
      let cx = leftX;
      kids.forEach(c => { placeX(c, cx); cx += subtreeW(c) + H_GAP; });
      n._x = (kids[0]._x + kids[kids.length - 1]._x) / 2;   // centre the parent over its children
    };
    const flowY = (n, y) => { n._y = y; const h = nodeHeights[n.id] || NODE_H; kidsOf(n.id).forEach(c => flowY(c, y + h + V_GAP)); };
    let rootX = 60;
    nodes.filter(n => !n.parent).forEach(r => { placeX(r, rootX); flowY(r, 48); rootX += subtreeW(r) + H_GAP; });
  })();
  const nodeById = (id) => nodes.find(n => n.id === id);
  const childrenOf = (pid) => ivr.nodes.filter(n => n.parent === pid);
  const nextKeyFor = (pid) => { const used = new Set(childrenOf(pid).flatMap(c => ivrNodeKeys(c))); return KEYS.find(k => !used.has(k)) || '#'; };
  const hasRoot = ivr.nodes.some(n => !n.parent);

  const zoomIn  = () => setZoom(z => Math.min(1.6, +(z + 0.15).toFixed(2)));
  const zoomOut = () => setZoom(z => Math.max(0.5, +(z - 0.15).toFixed(2)));

  // Fit the whole tree into view: zoom so the nodes' bounding box fits (with padding), then
  // pan so it's centred. Handles wide tidy trees that don't fit at zoom 1.
  const recenter = () => {
    const el = scrollRef.current;
    const r = el ? el.getBoundingClientRect() : { width: 900, height: 520 };
    if (nodes.length === 0) { setZoom(1); setPan({ x: 0, y: 0 }); return; }
    const minX = Math.min(...nodes.map(n => n._x));
    const maxX = Math.max(...nodes.map(n => n._x + NODE_W));
    const minY = Math.min(...nodes.map(n => n._y));
    const maxY = Math.max(...nodes.map(n => n._y + (nodeHeights[n.id] || NODE_H)));
    const bw = Math.max(1, maxX - minX), bh = Math.max(1, maxY - minY);
    const PAD = 56;
    const z = Math.max(0.3, Math.min(1, +Math.min((r.width - 2 * PAD) / bw, (r.height - 2 * PAD) / bh).toFixed(2)));
    setZoom(z);
    setPan({ x: r.width / 2 - z * (minX + maxX) / 2, y: r.height / 2 - z * (minY + maxY) / 2 });
  };

  // Auto-fit the tree into view once, after every card's height has been measured, so the
  // builder opens showing the whole flow (a tidy tree can be wide and its root centred far in).
  const recenterRef = useRefIvr(recenter);
  recenterRef.current = recenter;
  const didFitRef = useRefIvr(false);
  useLayoutEffectIvr(() => {
    if (didFitRef.current || !nodes.length || !scrollRef.current) return;
    if (!nodes.every(n => nodeHeights[n.id] != null)) return;   // wait for measured heights
    didFitRef.current = true;
    recenterRef.current();
  });

  // Full screen: blow the canvas up to fill the viewport, and re-fit the tree to the new size.
  // Toggling back off restores the canvas to its original in-page position (a fresh re-fit).
  const toggleFullscreen = () => setFullscreen(v => !v);
  const fsMountRef = useRefIvr(false);
  useLayoutEffectIvr(() => {
    if (!fsMountRef.current) { fsMountRef.current = true; return; }   // skip first render
    // Re-fit once the fixed full-screen (or restored in-page) layout has settled. setTimeout —
    // not requestAnimationFrame — so it still fires in a backgrounded/headless tab where rAF is paused.
    const id = setTimeout(() => recenterRef.current && recenterRef.current(), 60);
    return () => clearTimeout(id);
  }, [fullscreen]);
  useEffectIvr(() => {
    if (!fullscreen) return;
    const onKey = (e) => { if (e.key === 'Escape') setFullscreen(false); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [fullscreen]);

  // Pan the canvas like a map: grab the empty grid and drag in any direction.
  const onCanvasDown = (e) => {
    if (e.button !== 0) return;
    if (treeView) return; // tree view scrolls/clicks normally — no canvas panning
    if (e.target.closest('.ivr-cnode, .ivr-canvas-add, .ivr-canvas-zoom, .ivr-canvas-tree, .ivr-canvas-sim, .ivr-tv')) return; // let nodes/buttons handle their own clicks
    const d = dragRef.current;
    d.active = true; d.sx = e.clientX; d.sy = e.clientY; d.px = pan.x; d.py = pan.y;
    setPanning(true);
    e.preventDefault();
  };
  useEffectIvr(() => {
    const onMove = (e) => {
      const d = dragRef.current;
      if (!d.active) return;
      setPan({ x: d.px + (e.clientX - d.sx), y: d.py + (e.clientY - d.sy) });
    };
    const onUp = () => { if (dragRef.current.active) { dragRef.current.active = false; setPanning(false); } };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
  }, []);

  // Seed Step 2's canvas ONCE per IVR type, before paint (no flash of the old canvas).
  // On the FIRST entry for a template — or after the type is changed in Step 1 — we
  // start from a clean canvas and make sure the default variables are available. On
  // RE-ENTRY with the SAME type (coming back from Step 3, or from Step 1 without
  // changing the type) we KEEP every node the user already built instead of wiping
  // their work. `_seededType` records which type the current canvas was seeded for.
  useLayoutEffectIvr(() => {
    if (ivr._seededType === ivr.type) return;   // already seeded for this type → preserve the nodes
    updateIvr({ nodes: [], variables: (ivr.variables || []).length ? ivr.variables : IVR_DEFAULT_VARS, _seededType: ivr.type });
    // eslint-disable-next-line
  }, []);

  // The Add panel shrinks the canvas; recompute the ENTRY (centred) draft position
  // AFTER that reflow so the first card lands dead-centre of the *visible* stage.
  useLayoutEffectIvr(() => {
    if (!panelOpen || editId || parentForNew) return;
    const el = scrollRef.current; if (!el) return;
    const r = el.getBoundingClientRect();
    setDraftPos({ x: (r.width / 2 - pan.x) / zoom - NODE_W / 2, y: (r.height / 2 - pan.y) / zoom - NODE_H / 2 });
    // eslint-disable-next-line
  }, [panelOpen, editId, parentForNew]);

  // Where the about-to-be-created node sits (entry = centre of view; child = below its parent).
  const newNodePos = (parentId) => {
    const parent = parentId ? ivr.nodes.find(n => n.id === parentId) : null;
    if (!parent) {
      // First/entry node: drop it in the DEAD CENTRE of the visible stage.
      const el = scrollRef.current; const r = el ? el.getBoundingClientRect() : { width: 900, height: 520 };
      return { x: (r.width / 2 - pan.x) / zoom - NODE_W / 2, y: (r.height / 2 - pan.y) / zoom - NODE_H / 2 };
    }
    const sibs = childrenOf(parentId);
    return {
      x: (parent.x != null ? parent.x : 60) + sibs.length * (NODE_W + H_GAP) - NODE_W * 0.15,
      y: (parent.y != null ? parent.y : 48) + (nodeHeights[parentId] || NODE_H) + V_GAP,
    };
  };

  // Clicking any "Add" affordance opens the "Add node" side panel with an EMPTY
  // node previewed on the canvas; fill it in and Add to commit. Entry when
  // parentId is null, otherwise a keyed child of that parent.
  const openAdd = (parentId) => {
    setEditId(null);
    setParentForNew(parentId || null);
    setDraft({ name: '', voice: '', timeout: '', varType: '', varName: '', keys: parentId ? [nextKeyFor(parentId)] : [], kind: 'voice', segments: [] });
    setDraftPos(newNodePos(parentId || null));
    setPanelTab('node');
    setPanelOpen(true);
  };
  const openEdit = (n) => { const firstRec = (n.segments || []).find(s => s.kind === 'recording'); setEditId(n.id); setParentForNew(null); setDraft({ name: n.label || '', voice: n.voice || (firstRec ? firstRec.name : ''), timeout: n.timeout != null ? String(n.timeout) : '', varType: n.varType || '', varName: n.varName || '', keys: ivrNodeKeys(n), kind: n.varName ? 'variable' : 'voice', segments: (n.segments || []).map(s => ({ ...s })) }); setPanelTab(readOnly && ivr.type === 'dynamic' ? 'variables' : 'node'); setPanelOpen(true); };
  const closePanel = () => { setPanelOpen(false); setEditId(null); setParentForNew(null); };

  // Upload Voice → open the OS file picker; the chosen file's name becomes the sound.
  const onVoiceFile = (e) => {
    const f = e.target.files && e.target.files[0];
    if (f) {
      setMyVoices(opts => opts.includes(f.name) ? opts : [...opts, f.name]);
      const segIdx = uploadSegIdxRef.current;
      if (segIdx != null) {
        setDraft(d => ({ ...d, segments: (d.segments || []).map((s, i) => i === segIdx ? { ...s, name: f.name } : s) }));
        uploadSegIdxRef.current = null;
      } else {
        setDraft(d => ({ ...d, voice: f.name }));
      }
    }
    e.target.value = '';
  };

  const isDyn = ivr.type === 'dynamic';
  // A node only needs a NAME to be saved — the voice recording is OPTIONAL, so a
  // node can act as a pure router (e.g. a keypad-triggered Jump with no voice).
  const canSave = (draft.name || '').trim().length > 0;
  // A node can route the caller on with at most ONE "Go to node" — disable "+ Node" once added.
  const draftHasGoto = (draft.segments || []).some(s => s.kind === 'goto');
  // A node is EITHER a Jump OR voice/variable content — never both (mutually exclusive).
  const draftHasContent = isDyn
    ? (draft.segments || []).some(s => s.kind === 'recording' || s.kind === 'variable')
    : !!(draft.voice || '').trim();

  // The Voice chooser (tabbed picker + upload) — shared by the static form and the
  // dynamic "Voice" type so they look and behave identically.
  const voiceField = (
    <div className="tpl-field">
      <div className="ivr-voice-label-row">
        <label className="tpl-field-label">{t.ivrSoundLbl || 'Voice'}</label>
        <button type="button" className="ivr-voice-reset" title={t.reset || 'Reset'} onClick={() => setDraft(d => ({ ...d, voice: '' }))}>{IcReset}</button>
      </div>
      <div className="ivr-voice-row">
        <IvrVoicePicker
          value={draft.voice}
          onChange={(v) => setDraft(d => ({ ...d, voice: v }))}
          myVoices={myVoices}
          sharedVoices={sharedVoices}
          t={t}
        />
        <button type="button" className="ivr-upload-voice" title={t.ivrUploadVoice || 'Upload Voice'} onClick={() => fileRef.current && fileRef.current.click()}>{IcUpload}</button>
        <input ref={fileRef} type="file" accept="audio/*,.wav,.mp3,.m4a,.ogg" style={{ display: 'none' }} onChange={onVoiceFile} />
      </div>
    </div>
  );

  // ---- Dynamic node content: an ordered list of voice + variable segments ----
  const addSeg = (seg) => setDraft(d => ({ ...d, segments: [...(d.segments || []), seg] }));
  const updateSeg = (idx, patch) => setDraft(d => ({ ...d, segments: (d.segments || []).map((s, i) => i === idx ? { ...s, ...patch } : s) }));
  const removeSeg = (idx) => setDraft(d => ({ ...d, segments: (d.segments || []).filter((_, i) => i !== idx) }));
  // Reorder segments — drag the row number, or use the up / down arrows.
  const [dragIdx, setDragIdx] = useStateIvr(null);
  const [overIdx, setOverIdx] = useStateIvr(null);
  const moveSeg = (from, to) => setDraft(d => { const segs = [...(d.segments || [])]; if (from == null || to == null || from < 0 || from >= segs.length || to < 0 || to >= segs.length || from === to) return d; const [m] = segs.splice(from, 1); segs.splice(to, 0, m); return { ...d, segments: segs }; });
  const openUploadForSeg = (idx) => { uploadSegIdxRef.current = idx; if (fileRef.current) fileRef.current.click(); };

  // Short content summary for a node card (its segments joined in play order).
  const contentSummary = (n) => {
    const segs = n.segments || [];
    if (segs.length) return segs.map(s => s.kind === 'recording' ? (s.name || (t.ivrRecording || 'recording')) : `{${s.key}}`).join(' · ');
    return n.voice || (n.varName ? `{${n.varName}}` : (t.ivrNoVoice || 'sound name'));
  };

  // Render a node's content as a sequence of chips: voice = name pill, variable = {key}.
  // Render a node's segments as chips. When `nodeId` is given (a committed card), each
  // RECORDING chip becomes its own play/pause button so the user can hear each sound
  // individually; a thin bar animates for that segment's duration, then resets.
  const isVoiceSeg = (s) => !!s && (s.kind === 'recording' || s.kind === 'variable');
  const segDurOf = (seg, nodeDur) => (seg && seg.kind === 'variable') ? 1.6 : ((seg && seg.dur) || nodeDur || 8);
  const renderSegChips = (segments, fallbackText, nodeId, nodeDur) => {
    const segs = (segments || []).filter(Boolean);
    if (!segs.length) return <span className="ivr-cnode-seg-empty">{fallbackText || (t.ivrNoVoice || 'sound name')}</span>;
    // After the current segment finishes, jump to the next playable one — or stop at the end.
    const advance = (from) => { for (let j = from; j < segs.length; j++) { if (isVoiceSeg(segs[j])) { setPlay({ nodeId, idx: j, paused: false }); return; } } setPlay(null); };
    return segs.map((seg, i) => {
      const playable = !!nodeId && isVoiceSeg(seg);
      const active = playable && play && play.nodeId === nodeId && play.idx === i;
      const paused = active && play.paused;
      const onClick = playable ? (e) => { e.stopPropagation(); if (active) setPlay(p => ({ ...p, paused: !p.paused })); else setPlay({ nodeId, idx: i, paused: false }); } : undefined;
      // The bar freezes where it reached on pause (animation-play-state) and resumes from there.
      const bar = active && <span className="ivr-cseg-bar" style={{ animationDuration: segDurOf(seg, nodeDur) + 's', animationPlayState: paused ? 'paused' : 'running' }} onAnimationEnd={() => advance(i + 1)} />;
      if (seg.kind === 'recording') {
        return (
          <span
            key={i}
            className={`ivr-cseg ivr-cseg-voice ${playable ? 'is-playable' : ''} ${active ? 'is-playing' : ''}`}
            title={playable ? `${active && !paused ? (t.pause || 'Pause') : (t.play || 'Play')} — ${seg.name || 'voice'}` : (seg.name || 'voice')}
            onClick={onClick}
          >
            <span className="ivr-cseg-play">{playable ? (active && !paused ? IcPauseSm : IcPlaySm) : IcVoice}</span>
            <span className="ivr-cseg-nm">{seg.name || (t.ivrRecording || 'voice')}</span>
            {bar}
          </span>
        );
      }
      if (seg.kind === 'goto') return <span key={i} className="ivr-cseg ivr-cseg-goto" title={t.ivrGotoTitle || 'Go to node'}>↻ {(nodeById(seg.target) || {}).label || (t.ivrNode || 'node')}</span>;
      return (
        <span key={i} className={`ivr-cseg ivr-cseg-var ${playable ? 'is-playable' : ''} ${active ? 'is-playing' : ''}`} title={'{' + (seg.key || '') + '}'} onClick={onClick}>
          {'{' + (seg.key || '?') + '}'}
          {bar}
        </span>
      );
    });
  };

  // A node/draft "has audio" only when it contains at least one named recording.
  const segHasRecording = (segments) => (segments || []).some(s => s.kind === 'recording' && (s.name || '').trim());

  // ----- Full-call simulation on the canvas -----
  // Play every node's voice in DFS order, following (centring) the active node. Reuses
  // the per-node `play` mechanism: when a node's voice finishes (play → null), advance
  // to the next node. Pause freezes the active node's bar; Stop clears the queue.
  const [simActive, setSimActive] = useStateIvr(false);
  const simRef = useRefIvr({ list: [], pos: 0 });
  const nodeVoiceSegs = (n) => (n && n.segments && n.segments.length) ? n.segments : (n && n.voice ? [{ kind: 'recording', name: n.voice, dur: n.dur }] : []);
  const nodeHasVoice = (n) => nodeVoiceSegs(n).some(isVoiceSeg);
  const playNodeForSim = (id) => { const segs = nodeVoiceSegs(nodeById(id)); for (let j = 0; j < segs.length; j++) { if (isVoiceSeg(segs[j])) { setPlay({ nodeId: id, idx: j, paused: false }); return true; } } return false; };
  const playFullCall = () => {
    const list = ivrDfsNodes(buildIvrTree(ivr.nodes)).filter(nodeHasVoice).map(n => n.id);
    if (!list.length) return;
    simRef.current = { list, pos: 0 };
    setSelectedId(null);
    setSimActive(true);
    playNodeForSim(list[0]);
  };
  const stopSim = () => { simRef.current = { list: [], pos: 0 }; setSimActive(false); setPlay(null); };
  const toggleSimPause = () => setPlay(p => p ? { ...p, paused: !p.paused } : p);
  // When the active node finishes (play → null) mid-sim, advance to the next node (or end).
  useEffectIvr(() => {
    if (!simActive) return;
    if (play == null) {
      const { list, pos } = simRef.current;
      const next = pos + 1;
      if (next < list.length) { simRef.current = { list, pos: next }; playNodeForSim(list[next]); }
      else { simRef.current = { list: [], pos: 0 }; setSimActive(false); }
    }
  }, [play, simActive]);
  // Keep the currently-playing node centred in the canvas during a sim.
  useEffectIvr(() => {
    if (!simActive || !play || !play.nodeId) return;
    const n = nodeById(play.nodeId), sc = scrollRef.current;
    if (!n || !sc) return;
    const r = sc.getBoundingClientRect(), h = nodeHeights[n.id] || NODE_H;
    setPan({ x: r.width / 2 - (n._x + NODE_W / 2) * zoom, y: r.height / 2 - (n._y + h / 2) * zoom });
  }, [simActive, play && play.nodeId]);

  // Add a dynamic variable (from the modal): store it globally and select it on the
  // segment row that opened the modal.
  const addVariable = ({ key, type, sample }) => {
    updateIvr({ variables: [...(ivr.variables || []), { key, type, sample }] });
    if (varModalSegIdx != null) updateSeg(varModalSegIdx, { key });
    setVarModalOpen(false);
    setVarModalSegIdx(null);
  };

  const saveNode = () => {
    if (!canSave) return;
    const name = draft.name.trim();
    const timeout = draft.timeout === '' ? null : (parseInt(draft.timeout, 10) || 0);
    // Static = one recording (draft.voice). Dynamic = the ordered segment list
    // (multiple recordings and/or variables) the caller hears, built in the panel.
    const staticGoto = (draft.segments || []).find(s => s.kind === 'goto' && (s.target || '').trim());
    // XOR: a node is EITHER a Jump OR voice/variable content — never both. A jump wins.
    const segments = isDyn
      ? (() => {
          const valid = (draft.segments || []).filter(s => s.kind === 'recording' ? (s.name || '').trim() : s.kind === 'goto' ? (s.target || '').trim() : (s.key || '').trim());
          const goto = valid.find(s => s.kind === 'goto');
          return (goto ? [goto] : valid.filter(s => s.kind !== 'goto'))
            .map(s => s.kind === 'recording' ? { kind: 'recording', name: s.name, dur: s.dur || 8 } : s.kind === 'goto' ? { kind: 'goto', target: s.target } : { kind: 'variable', key: s.key });
        })()
      : staticGoto
        ? [{ kind: 'goto', target: staticGoto.target }]
        : ((draft.voice || '') ? [{ kind: 'recording', name: draft.voice, dur: 8 }] : []);
    const voice = (isDyn || staticGoto) ? '' : (draft.voice || '');
    if (editId) {
      updateIvr({ nodes: ivr.nodes.map(n => {
        if (n.id !== editId) return n;
        const keys = n.parent ? (draft.keys.length ? draft.keys : ivrNodeKeys(n)) : [];
        return { ...n, label: name, key: keys[0] || null, keys, voice, varType: '', varName: '', timeout, segments };
      }) });
    } else {
      const parent = parentForNew ? ivr.nodes.find(n => n.id === parentForNew) : null;
      const keys = parent ? (draft.keys.length ? draft.keys : [nextKeyFor(parentForNew)]) : [];
      const node = {
        id: (!parent && !hasRoot) ? 'n_root' : ivrUid(),
        parent: parentForNew || null,
        key: keys[0] || null, keys,
        label: name, voice, varType: '', varName: '', timeout, dur: 8,
        segments,
        terminal: null, x: draftPos.x, y: draftPos.y,
      };
      updateIvr({ nodes: [...ivr.nodes, node] });
      if (!parent) setSelectedId(node.id);   // entry → select it; a child keeps the parent selected
    }
    closePanel();
  };

  // Auto-commit an open, valid node draft when this step unmounts — i.e. clicking
  // "Next" (→ step 3) or "Previous" (→ step 1) while a node panel is open. Without
  // this, the node you were creating/editing in the panel would be silently lost
  // when you navigate away and come back to step 2. `commitDraftRef` always holds
  // the latest draft state (refreshed every render); the unmount cleanup runs it.
  const commitDraftRef = useRefIvr(() => {});
  useEffectIvr(() => { commitDraftRef.current = () => { if (panelOpen && canSave) saveNode(); }; });
  useEffectIvr(() => () => { commitDraftRef.current(); }, []);

  // How many nodes would a delete remove (the node + its whole subtree).
  const subtreeIds = (id) => {
    const remove = new Set([id]); let changed = true;
    while (changed) { changed = false; ivr.nodes.forEach(n => { if (n.parent && remove.has(n.parent) && !remove.has(n.id)) { remove.add(n.id); changed = true; } }); }
    return remove;
  };
  // Deleting asks for confirmation first; the actual removal runs on confirm.
  const requestDelete = (id) => setConfirmDel(id);
  const doDeleteNode = (id) => {
    const remove = subtreeIds(id);
    updateIvr({ nodes: ivr.nodes.filter(n => !remove.has(n.id)) });
    if (remove.has(editId)) closePanel();
    if (remove.has(selectedId)) setSelectedId(null);
    setConfirmDel(null);
  };

  const edges = nodes.filter(n => n.parent && nodeById(n.parent));
  const draftParent = (panelOpen && !editId && parentForNew) ? nodeById(parentForNew) : null;
  const draftKeys = parentForNew ? (draft.keys.length ? draft.keys : [nextKeyFor(parentForNew)]) : [];
  // "Go to node" routes — drawn as dashed return/repeat lines on the canvas.
  const gotoEdges = nodes.flatMap(n => (n.segments || []).filter(s => s.kind === 'goto' && s.target && nodeById(s.target)).map(s => ({ from: n, to: nodeById(s.target) })));

  // A return/goto edge is drawn as ONE dashed connector anchored at the vertical centre of
  // BOTH cards' side edges, routed like a rounded "racetrack": OUT from the source's row,
  // straight UP/DOWN a clear vertical CHANNEL beyond the cards, then back IN to the target
  // ALONG THE TARGET'S OWN ROW (entering on the target's row keeps it off whatever sits on the
  // row just below it). The channel runs down ONE SIDE — left or right, whichever crosses fewer
  // cards (ties → the more compact route). The source and target are INCLUDED when finding that
  // outer edge, so the channel always clears them and each end anchors on the edge that FACES
  // the channel — that's what stops the line from cutting back across its own card. Rendered
  // ABOVE the cards with a ring anchor at each end.
  const gotoGeom = (f, tg) => {
    const fh = nodeHeights[f.id] || NODE_H, th = nodeHeights[tg.id] || NODE_H;
    const fcy = f._y + fh / 2, tcy = tg._y + th / 2;   // each card's vertical centre
    if (f.id === tg.id) {                       // self-loop ("Repeat") — small curl off the left edge
      const x = f._x, y = fcy;
      return { d: `M ${x} ${y - 16} C ${x - 76} ${y - 40}, ${x - 76} ${y + 40}, ${x} ${y + 16}`,
               lx: x - 54, ly: y, a1: [x, y - 16], a2: [x, y + 16] };
    }
    const NW = NODE_W, yLo = Math.min(fcy, tcy), yHi = Math.max(fcy, tcy);
    // Occupied x-intervals across the band (INCLUDING source & target), merged into solid
    // blocks; the gaps between them are the clear lanes a channel can sit in.
    const bandIv = nodes.filter(n => { const h = nodeHeights[n.id] || NODE_H; return n._y < yHi && n._y + h > yLo; })
                        .map(n => [n._x, n._x + NW]).sort((a, b) => a[0] - b[0]);
    const occ = [];
    for (const iv of bandIv) {
      if (!occ.length || iv[0] > occ[occ.length - 1][1] + 1) occ.push([iv[0], iv[1]]);
      else occ[occ.length - 1][1] = Math.max(occ[occ.length - 1][1], iv[1]);
    }
    const candidates = [occ[0][0] - 34, occ[occ.length - 1][1] + 34];   // the lane just beyond everything, left & right
    const rects = nodes.filter(n => n.id !== f.id && n.id !== tg.id)
                       .map(n => ({ x: n._x, y: n._y, w: NW, h: nodeHeights[n.id] || NODE_H }));
    // does a horizontal run at height `y` between x = xa..xb pass through a card's interior?
    const hitH = (r, y, xa, xb) => (r.y + 6 < y && y < r.y + r.h - 6) && (r.x + 6 < Math.max(xa, xb)) && (r.x + r.w - 6 > Math.min(xa, xb));
    const fcx = f._x + NW / 2, tcx = tg._x + NW / 2;
    let best = null;
    for (const chX of candidates) {
      const p1x = chX >= fcx ? f._x + NW : f._x;        // anchor on the edge that faces the channel
      const p2x = chX >= tcx ? tg._x + NW : tg._x;
      let c = 0;                                        // only the two short IN/OUT runs can clip a neighbour
      for (let k = 0; k < rects.length; k++) { if (hitH(rects[k], fcy, p1x, chX) || hitH(rects[k], tcy, p2x, chX)) c++; }
      const detour = Math.abs(chX - p1x) + Math.abs(chX - p2x);
      if (!best || c < best.c || (c === best.c && detour < best.detour)) best = { p1x, p2x, chX, c, detour };
    }
    const { p1x, p2x, chX } = best;
    const sgnX = chX >= p1x ? 1 : -1, sgnY = Math.sign(tcy - fcy) || 1;
    const r = Math.max(2, Math.min(28, Math.abs(chX - p1x) / 2, Math.abs(chX - p2x) / 2, Math.abs(tcy - fcy) / 2));
    const d = `M ${p1x} ${fcy}`
            + ` L ${chX - sgnX * r} ${fcy} Q ${chX} ${fcy} ${chX} ${fcy + sgnY * r}`   // out, round into the channel
            + ` L ${chX} ${tcy - sgnY * r} Q ${chX} ${tcy} ${chX - sgnX * r} ${tcy}`   // up/down the channel, round onto target row
            + ` L ${p2x} ${tcy}`;                                                      // in along the target's row
    return { d, lx: chX, ly: (fcy + tcy) / 2, a1: [p1x, fcy], a2: [p2x, tcy] };
  };

  // ----- Edge-label layout (with de-overlap) -----
  // "Press N" (parent→child) and "↻ Return/Repeat" (goto) pills are positioned
  // independently along different curves, so where edges cross they can land on
  // top of each other (e.g. "Press 3" under "↻ Return"). Build them as data, then
  // nudge any colliding pills apart VERTICALLY so every label stays readable while
  // keeping its horizontal position on its own edge.
  const LBL_H = 26;
  const estLblW = (txt) => (txt || '').length * 6.6 + 22;   // rough pill width from text length
  // One "Press <keys>" pill PER child edge, sitting on that edge between the parent and the
  // child — exactly like the create flow. Each pill shows that single child's keypad key(s),
  // so the numbering is unambiguous (the pill belongs to the edge it sits on).
  const edgeLabels = [
    ...edges.map(n => {
      const p = nodeById(n.parent);
      const text = `${t.ivrPress || 'Press'} ${ivrKeysLabel(ivrNodeKeys(n))}`;
      return { id: 'l' + n.id, cls: 'ivr-edge-label', text,
        x: (p._x + NODE_W / 2 + n._x + NODE_W / 2) / 2,
        y: (p._y + (nodeHeights[p.id] || NODE_H) + n._y) / 2,
        w: estLblW(text) };
    }),
    ...gotoEdges.map((e, i) => {
      const self = e.from.id === e.to.id;
      const g = gotoGeom(e.from, e.to);                 // pill sits on the curve's midpoint
      const text = `↻ ${self ? (t.ivrRepeat || 'Repeat') : (t.ivrReturn || 'Return')}`;
      return { id: 'gl' + i, cls: 'ivr-edge-label is-return', text, x: g.lx, y: g.ly, w: estLblW(text) };
    }),
  ];
  // Greedy separation: push any overlapping pair apart along the vertical axis.
  for (let iter = 0; iter < 8; iter++) {
    let moved = false;
    for (let a = 0; a < edgeLabels.length; a++) {
      for (let b = a + 1; b < edgeLabels.length; b++) {
        const A = edgeLabels[a], B = edgeLabels[b];
        const ox = (A.w + B.w) / 2 + 6 - Math.abs(A.x - B.x);   // horizontal overlap (+6px gap)
        const oy = LBL_H + 4 - Math.abs(A.y - B.y);             // vertical overlap (+4px gap)
        if (ox > 0 && oy > 0) {
          const push = oy / 2 + 0.5;
          if (A.y <= B.y) { A.y -= push; B.y += push; } else { A.y += push; B.y -= push; }
          moved = true;
        }
      }
    }
    if (!moved) break;
  }

  // ----- Tree (outline) view — a nested, collapsible list of the same nodes -----
  // Renders the hierarchy as indented "IVR List" rows (chevron · name · Press key ·
  // delete). Clicking a row's body opens its editor; the chevron collapses its branch.
  const renderTreeRows = (parentId, depth) => {
    const kids = ivr.nodes.filter(n => (n.parent || null) === parentId);
    return kids.flatMap((node) => {
      const hasKids = ivr.nodes.some(n => n.parent === node.id);
      const isCol = !!treeCollapsed[node.id];
      // Same content the canvas card shows — voice / variable / goto chips (recordings
      // are individually playable). Static nodes fold their single voice into a chip.
      const segs = (node.segments && node.segments.length) ? node.segments : (node.voice ? [{ kind: 'recording', name: node.voice, dur: node.dur }] : []);
      const row = (
        <div key={node.id} className="ivr-tv-row" style={{ marginInlineStart: (depth * 28) + 'px' }}>
          <div className={`ivr-tv-card ${selectedId === node.id ? 'is-active' : ''}`}>
            <button type="button" className={`ivr-tv-chev ${hasKids ? '' : 'is-leaf'}`} disabled={!hasKids}
              title={hasKids ? (isCol ? (t.expand || 'Expand') : (t.collapse || 'Collapse')) : ''}
              onClick={() => setTreeCollapsed(c => ({ ...c, [node.id]: !c[node.id] }))}>
              {hasKids ? (isCol ? IcChevRightTree : IcChevDownTree) : null}
            </button>
            <div className="ivr-tv-main" onClick={canOpenNode ? () => { setSelectedId(node.id); openEdit(node); } : undefined}>
              <div className="ivr-tv-titlerow">
                <span className="ivr-tv-name">{node.label || (t.ivrUntitled || 'Untitled')}</span>
                {node.parent && <span className="ivr-tv-key">{(t.ivrPress || 'Press')} {ivrNodeKeys(node).join(' / ')}</span>}
              </div>
              {segs.length > 0 && <div className="ivr-tv-segs ivr-cnode-segs">{renderSegChips(segs, node.voice, node.id, node.dur)}</div>}
            </div>
            {!readOnly && <button type="button" className="ivr-tv-del" title={t.delete || 'Delete'} onClick={(e) => { e.stopPropagation(); requestDelete(node.id); }}>{IvrIcTrash}</button>}
          </div>
        </div>
      );
      return (hasKids && !isCol) ? [row, ...renderTreeRows(node.id, depth + 1)] : [row];
    });
  };

  // DTMF keypad selector — shown only for CHILD nodes (the entry/root node has no key).
  // The node's own key is highlighted; keys already used by sibling nodes are disabled.
  const editingNode = editId ? ivr.nodes.find(n => n.id === editId) : null;
  const keyParentId = editId ? (editingNode ? editingNode.parent : null) : parentForNew;
  const showKeypad = panelOpen && (editId ? !!(editingNode && editingNode.parent) : !!parentForNew);
  const takenKeys = new Set(ivr.nodes.filter(n => n.parent === keyParentId && n.id !== editId).flatMap(n => ivrNodeKeys(n)));
  // The root / entry node (no parent) gets NO "Jump" option.
  const isRootNode = panelOpen && (editId ? !!(editingNode && !editingNode.parent) : !parentForNew);

  // Jump targets exclude the node being edited (a node can't jump to itself); a
  // brand-new node (editId == null) isn't in ivr.nodes yet, so nothing is excluded.
  const jumpTargetOptions = ivr.nodes.filter(nd => nd.id !== editId).map(nd => ({ value: nd.id, label: nd.label || (t.ivrUntitled || 'Untitled') }));

  return (
    <div ref={stepRef} className={`ivr-canvas-step ${panelOpen ? 'with-panel' : ''} ${resizing ? 'is-resizing' : ''} ${fullscreen ? 'is-fullscreen' : ''}`}>
      <div
        ref={scrollRef}
        className={`ivr-canvas-scroll ${panning ? 'is-panning' : ''}`}
        onMouseDown={onCanvasDown}
        style={{ backgroundPosition: `${pan.x}px ${pan.y}px`, backgroundSize: `${(22 * zoom).toFixed(2)}px ${(22 * zoom).toFixed(2)}px` }}
      >
        <div className="ivr-canvas" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: '0 0', transition: panning ? 'none' : undefined }}>
          {nodes.length === 0 && !panelOpen && (
            <div className="ivr-canvas-hint">{t.ivrCanvasHint || 'Click + to create your first IVR node'}</div>
          )}

          {/* curved parent → child connectors */}
          <svg className="ivr-edges" width="6000" height="4000">
            {edges.map(n => {
              const p = nodeById(n.parent);
              const x1 = p._x + NODE_W / 2, y1 = p._y + (nodeHeights[p.id] || NODE_H);
              const x2 = n._x + NODE_W / 2, y2 = n._y;
              const dy = Math.max(46, (y2 - y1) * 0.5);
              return <path key={'e' + n.id} className="ivr-edge-path" d={`M ${x1} ${y1} C ${x1} ${y1 + dy}, ${x2} ${y2 - dy}, ${x2} ${y2}`} />;
            })}
            {draftParent && (() => {
              const x1 = draftParent._x + NODE_W / 2, y1 = draftParent._y + (nodeHeights[draftParent.id] || NODE_H);
              const x2 = draftPos.x + NODE_W / 2, y2 = draftPos.y;
              const dy = Math.max(46, (y2 - y1) * 0.5);
              return <path className="ivr-edge-path is-draft" d={`M ${x1} ${y1} C ${x1} ${y1 + dy}, ${x2} ${y2 - dy}, ${x2} ${y2}`} />;
            })()}
          </svg>
          {edgeLabels.map(L => (
            <div key={L.id} className={L.cls} style={{ left: L.x + 'px', top: L.y + 'px' }}>{L.text}</div>
          ))}
          {draftParent && (
            <div className="ivr-edge-label is-draft" style={{ left: ((draftParent._x + NODE_W / 2 + draftPos.x + NODE_W / 2) / 2) + 'px', top: ((draftParent._y + (nodeHeights[draftParent.id] || NODE_H) + draftPos.y) / 2) + 'px' }}>{(t.ivrPress || 'Press')} {ivrKeysLabel(draftKeys)}</div>
          )}

          {nodes.map(n => {
            const allSegs = (n.segments && n.segments.length) ? n.segments : (n.voice ? [{ kind: 'recording', name: n.voice, dur: n.dur }] : []);
            const gotoSeg = allSegs.find(s => s.kind === 'goto' && s.target);
            const contentSegs = allSegs.filter(s => s.kind !== 'goto');
            const hasVoice = contentSegs.some(isVoiceSeg);
            const nodeActive = play && play.nodeId === n.id;
            // Node play/pause: play the whole message from the first voice segment; pause toggles freeze/resume.
            const toggleNode = (e) => { e.stopPropagation(); if (nodeActive) { setPlay(p => ({ ...p, paused: !p.paused })); return; } for (let j = 0; j < contentSegs.length; j++) { if (isVoiceSeg(contentSegs[j])) { setPlay({ nodeId: n.id, idx: j, paused: false }); return; } } };
            return (
            <div
              key={n.id}
              className={`ivr-cnode ${selectedId === n.id ? 'is-active' : ''} ${!n.parent ? 'is-root' : ''} ${nodeActive ? 'is-playing' : ''}`}
              style={{ left: n._x + 'px', top: n._y + 'px', width: NODE_W + 'px', minHeight: NODE_H + 'px', cursor: canOpenNode ? 'pointer' : 'inherit' }}
              ref={(el) => { if (el) { const h = el.offsetHeight; if (nodeHeights[n.id] !== h) setNodeHeights(prev => prev[n.id] === h ? prev : { ...prev, [n.id]: h }); } }}
              onClick={canOpenNode ? () => { setSelectedId(n.id); openEdit(n); } : undefined}
            >
              <div className="ivr-cnode-titlerow">
                <div className="ivr-cnode-title">{n.label || 'Voice'}</div>
                {gotoSeg && <span className="ivr-cnode-goto-chip" title={t.ivrGotoTitle || 'Go to node'}>↻ {(nodeById(gotoSeg.target) || {}).label || (t.ivrNode || 'node')}</span>}
              </div>
              {contentSegs.length > 0 && <div className="ivr-cnode-segs">{renderSegChips(contentSegs, n.voice, n.id, n.dur)}</div>}
              <div className="ivr-cnode-row">
                {hasVoice && (
                  <button type="button" className={`ivr-cnode-playall ${nodeActive && !play.paused ? 'is-playing' : ''}`} title={nodeActive ? (play.paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.ivrPlayMsg || 'Play the whole message')} onClick={toggleNode}>
                    {nodeActive && !play.paused ? IcPauseSm : IcPlaySm}
                    <span>{nodeActive ? (play.paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.play || 'Play')}</span>
                  </button>
                )}
                <span className="ivr-player-spacer" />
                {!readOnly && (<>
                  <button type="button" className="ivr-cnode-add" title={t.ivrAddSub || 'Add sub-node'} onClick={(e) => { e.stopPropagation(); setSelectedId(n.id); openAdd(n.id); }}>{IcPlusBig}</button>
                  <button type="button" className="ivr-cnode-del" title={t.delete || 'Delete'} onClick={(e) => { e.stopPropagation(); requestDelete(n.id); }}>{IvrIcTrash}</button>
                </>)}
              </div>
              {!readOnly && <button type="button" className="ivr-cnode-dot" title={t.ivrAddSub || 'Add sub-node'} onClick={(e) => { e.stopPropagation(); setSelectedId(n.id); openAdd(n.id); }} />}
            </div>
            );
          })}

          {/* empty preview of the node being added — mirrors the "Add node" panel live */}
          {panelOpen && !editId && (
            <div className={`ivr-cnode is-active is-draft ${!parentForNew ? 'is-root' : ''}`} style={{ left: draftPos.x + 'px', top: draftPos.y + 'px', width: NODE_W + 'px', minHeight: NODE_H + 'px' }}>
              {(() => {
                const allSegs = isDyn ? (draft.segments || []) : (draft.voice ? [{ kind: 'recording', name: draft.voice }] : []);
                const gotoSeg = allSegs.find(s => s.kind === 'goto' && s.target);
                const contentSegs = allSegs.filter(s => s.kind !== 'goto');
                return (
                  <>
                    <div className="ivr-cnode-titlerow">
                      <div className="ivr-cnode-title">{draft.name ? draft.name : <span className="ivr-cnode-ph">{t.ivrNewNode || 'New node'}</span>}</div>
                      {gotoSeg && <span className="ivr-cnode-goto-chip" title={t.ivrGotoTitle || 'Go to node'}>↻ {(nodeById(gotoSeg.target) || {}).label || (t.ivrNode || 'node')}</span>}
                    </div>
                    <div className="ivr-cnode-segs">{renderSegChips(contentSegs, draft.voice || (t.ivrNoSound || 'No sound'))}</div>
                  </>
                );
              })()}
              {(isDyn ? segHasRecording(draft.segments) : !!(draft.voice || '').trim()) && (
                <div className="ivr-cnode-row">
                  <div className="ivr-player">
                    <span className="ivr-play">{IcPlay}</span>
                    <div className="ivr-track"><div className="ivr-track-fill" /></div>
                    <span className="ivr-time">{fmtDur(8)}</span>
                  </div>
                </div>
              )}
            </div>
          )}

          {/* return / goto edges — drawn ABOVE the cards so the dashed line is never
              chopped behind a node; one continuous, gentle curve per return */}
          <svg className="ivr-edges-over" width="6000" height="4000">
            {gotoEdges.map((e, i) => {
              const g = gotoGeom(e.from, e.to);
              return (
                <g key={'g' + i}>
                  <path className="ivr-edge-path is-return" d={g.d} />
                  <circle className="ivr-edge-anchor" cx={g.a1[0]} cy={g.a1[1]} r="5" />
                  <circle className="ivr-edge-anchor" cx={g.a2[0]} cy={g.a2[1]} r="5" />
                </g>
              );
            })}
          </svg>
        </div>

        {nodes.length > 0 && (
          <div className="ivr-canvas-sim">
            {simActive ? (<>
              <button type="button" className={`ivr-sim-btn ${play && !play.paused ? 'is-playing' : ''}`} onClick={toggleSimPause} title={play && play.paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')}>
                {play && !play.paused ? IcPauseSm : IcPlaySm}<span>{play && play.paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')}</span>
              </button>
              <button type="button" className="ivr-sim-stop" onClick={stopSim} title={t.ivrStopPlayback || 'Stop'} aria-label={t.ivrStopPlayback || 'Stop'}>{IcStopSm}</button>
            </>) : (
              <button type="button" className="ivr-sim-btn" onClick={playFullCall} title={t.ivrSimHint || 'Play the whole flow'}>{IcPlaySm}<span>{t.ivrSimCall || 'Simulate full call'}</span></button>
            )}
          </div>
        )}
        <button type="button" className={`ivr-canvas-tree ${treeView ? 'is-on' : ''}`} title={treeView ? (t.ivrCardView || 'Card view') : (t.ivrTreeView || 'Tree view')} onClick={() => setTreeView(v => !v)}>{IcTree}</button>
        {!readOnly && <button type="button" className="ivr-canvas-add" title={t.ivrCreate || 'Create'} onClick={() => { const sel = (selectedId && ivr.nodes.some(n => n.id === selectedId)) ? selectedId : null; openAdd(sel || (hasRoot ? ivr.nodes.find(n => !n.parent).id : null)); }}>{IcPlusBig}</button>}

        <div className="ivr-canvas-zoom">
          <button type="button" className={`ivr-zoom-btn ivr-fullscreen-btn ${fullscreen ? 'is-on' : ''}`} title={fullscreen ? (t.ivrExitFull || 'Exit full screen') : (t.ivrFullScreen || 'Full screen')} aria-label={fullscreen ? (t.ivrExitFull || 'Exit full screen') : (t.ivrFullScreen || 'Full screen')} onClick={toggleFullscreen}>{fullscreen ? IvrIcCompress : IvrIcExpand}</button>
          <button type="button" className="ivr-zoom-btn ivr-recenter-btn" title={t.ivrRecenter || 'Center view'} onClick={recenter}>{IvrIcCenter}</button>
          <button type="button" className="ivr-zoom-btn" title={t.zoomIn || 'Zoom in'} onClick={zoomIn} disabled={zoom >= 1.6}>{IcZoomIn}</button>
          <button type="button" className="ivr-zoom-btn" title={t.zoomOut || 'Zoom out'} onClick={zoomOut} disabled={zoom <= 0.5}>{IcZoomOut}</button>
        </div>

        {treeView && (
          <div className="ivr-tv">
            <div className="ivr-tv-head">
              <span className="ivr-tv-title">{IcTree}<span>{t.ivrListTitle || 'IVR List'}</span></span>
              <button type="button" className="ivr-tv-close" title={t.close || 'Close'} onClick={() => setTreeView(false)}>{IvrIcClose}</button>
            </div>
            <div className="ivr-tv-body">
              {nodes.length === 0
                ? <div className="ivr-tv-empty">{t.ivrCanvasHint || 'Click + to create your first IVR node'}</div>
                : renderTreeRows(null, 0)}
            </div>
          </div>
        )}
      </div>

      {panelOpen && (
        <div
          className={`ivr-split-resizer ${resizing ? 'dragging' : ''}`}
          role="separator"
          aria-orientation="vertical"
          aria-label={t.ivrResizePanel || 'Resize panel'}
          tabIndex={0}
          title={t.wbResizeHint || 'Drag to resize · double-click to reset'}
          onMouseDown={startPanelResize}
          onTouchStart={startPanelResize}
          onDoubleClick={() => setPanelW(null)}
          onKeyDown={onPanelResizeKey}
        >
          <span className="ivr-split-grip" aria-hidden="true" />
        </div>
      )}

      {panelOpen && (
        <div className="ivr-create-panel" style={panelW ? { width: panelW + 'px' } : undefined}>
          <div className="ivr-create-panel-head">
            {/* Read-only views can only edit Variables, so the panel is titled "Edit variable". */}
            <h4>{readOnly ? (t.ivrEditVariableTitle || 'Edit variable') : (editId ? (t.ivrEditTitle || 'Edit node') : (t.ivrAddNodeTitle || 'Add node'))}</h4>
            <button type="button" className="ivr-cpanel-close" title={t.close || 'Close'} onClick={closePanel}>{IvrIcClose}</button>
          </div>

          {/* Node/Variables tabs only when EDITING a dynamic flow. In read-only views the node
              form is removed entirely — only the Variables editor is shown (no tabs). */}
          {isDyn && !readOnly && (
            <div className="ivr-cpanel-tabs" role="tablist">
              <button type="button" className={`ivr-cpanel-tab ${panelTab === 'node' ? 'is-active' : ''}`} onClick={() => setPanelTab('node')}>{editId ? (t.ivrEditTitle || 'Edit node') : (t.ivrAddNodeTitle || 'Add node')}</button>
              <button type="button" className={`ivr-cpanel-tab ${panelTab === 'variables' ? 'is-active' : ''}`} onClick={() => setPanelTab('variables')}>{t.ivrTabVariables || 'Variables'}</button>
            </div>
          )}

          {(!readOnly && (!isDyn || panelTab === 'node')) ? (
          /* The node form (the IVR "tree") is editable only when NOT read-only. In More Details
             it renders disabled — view the node, but the tree can't be edited (variables can). */
          <fieldset className="ivr-cpanel-form" disabled={readOnly}>
          <div className="tpl-field">
            <label className="tpl-field-label">{t.ivrNodeNameLbl || 'Name'}</label>
            <input className="tpl-field-input" value={draft.name} placeholder={t.ivrNodeNamePh || 'e.g. Main menu'} autoFocus onChange={(e) => setDraft({ ...draft, name: e.target.value.slice(0, 60) })} />
          </div>

          {isDyn ? (
            <div className="tpl-field">
              <div className="ivr-seg-head">
                <label className="tpl-field-label">{t.ivrContentLbl || 'Message content'}</label>
                <div className="ivr-seg-actions">
                  <button type="button" className="ivr-seg-add-link" disabled={draftHasGoto} title={draftHasGoto ? (t.ivrJumpExcludesContent || 'Remove the jump first — a node is either a jump or a message') : ''} onClick={() => addSeg({ kind: 'recording', name: '' })}>{IcPlusSm} {t.ivrAddVoiceSeg || 'Voice'}</button>
                  <button type="button" className="ivr-seg-add-link" disabled={draftHasGoto} title={draftHasGoto ? (t.ivrJumpExcludesContent || 'Remove the jump first — a node is either a jump or a message') : ''} onClick={() => addSeg({ kind: 'variable', key: '' })}>{IcPlusSm} {t.ivrAddVariable || 'Variable'}</button>
                  {!isRootNode && <button type="button" className="ivr-seg-add-link" disabled={draftHasGoto || draftHasContent} onClick={() => addSeg({ kind: 'goto', target: '' })} title={draftHasGoto ? (t.ivrAddGotoUsed || 'Only one jump is allowed per node') : draftHasContent ? (t.ivrContentExcludesJump || 'Remove the voice/variable first — a node is either a message or a jump') : (t.ivrAddGotoHint || 'Jump the caller to an existing node')}>{IcPlusSm} {t.ivrAddGoto || 'Jump'}</button>}
                </div>
              </div>
              <div className="ivr-seg-builder">
                {(draft.segments || []).length === 0 && (
                  <div className="ivr-empty">{t.ivrSegEmpty || 'Add voices and variables to build the message.'}</div>
                )}
                {(draft.segments || []).map((seg, idx) => (
                  <div
                    key={idx}
                    className={`ivr-seg-edit ivr-seg-edit-${seg.kind} ${(dragIdx !== null && overIdx === idx && dragIdx !== idx) ? 'is-drop' : ''} ${dragIdx === idx ? 'is-dragging' : ''}`}
                    onDragOver={(e) => { if (dragIdx !== null) { e.preventDefault(); if (overIdx !== idx) setOverIdx(idx); } }}
                    onDrop={(e) => { e.preventDefault(); let from = dragIdx; if (from === null && e.dataTransfer) { const raw = e.dataTransfer.getData('text/plain'); if (raw !== '' && raw != null) from = parseInt(raw, 10); } if (from !== null && !isNaN(from)) moveSeg(from, idx); setDragIdx(null); setOverIdx(null); }}
                  >
                    <span className="ivr-seg-idx" draggable title={t.ivrDragReorder || 'Drag to reorder'} onDragStart={(e) => { setDragIdx(idx); e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', String(idx)); } catch (_) {} }} onDragEnd={() => { setDragIdx(null); setOverIdx(null); }}>{idx + 1}</span>
                    <span className="ivr-seg-move">
                      <button type="button" className="ivr-seg-mv" disabled={idx === 0} title={t.ivrMoveUp || 'Move up'} onClick={() => moveSeg(idx, idx - 1)}><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 15 12 9 18 15"/></svg></button>
                      <button type="button" className="ivr-seg-mv" disabled={idx === (draft.segments || []).length - 1} title={t.ivrMoveDown || 'Move down'} onClick={() => moveSeg(idx, idx + 1)}><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button>
                    </span>
                    {seg.kind === 'recording' ? (
                      <>
                        <IvrVoicePicker value={seg.name} onChange={(v) => updateSeg(idx, { name: v })} myVoices={myVoices} sharedVoices={sharedVoices} t={t} />
                        <button type="button" className="ivr-upload-voice" title={t.ivrUploadVoice || 'Upload Voice'} onClick={() => openUploadForSeg(idx)}>{IcUpload}</button>
                      </>
                    ) : seg.kind === 'goto' ? (
                      <IvrSelect value={seg.target} onChange={(v) => updateSeg(idx, { target: v })} placeholder={t.ivrGotoPh || 'Go to node…'} options={jumpTargetOptions} emptyLabel={t.ivrJumpNoTargets || 'No other nodes yet — add one to jump to it.'} t={t} />
                    ) : (
                      <>
                        <IvrSelect value={seg.key} onChange={(v) => updateSeg(idx, { key: v })} options={(ivr.variables || []).map(v => ({ value: v.key, label: v.key, hint: ivrVarTypeLabel(v.type) }))} t={t} />
                        <button type="button" className="ivr-upload-voice" title={t.ivrAddVariable || 'Add variable'} onClick={() => { setVarModalSegIdx(idx); setVarModalOpen(true); }}>{IcPlusSm}</button>
                      </>
                    )}
                    <button type="button" className="ivr-seg-remove" title={t.delete || 'Remove'} onClick={() => removeSeg(idx)}>{IvrIcTrash}</button>
                  </div>
                ))}
              </div>
              <input ref={fileRef} type="file" accept="audio/*,.wav,.mp3,.m4a,.ogg" style={{ display: 'none' }} onChange={onVoiceFile} />
            </div>
          ) : (
            <>
              {/* A node is EITHER a voice OR a jump — hide the voice picker once a jump is added. */}
              {!draftHasGoto && voiceField}
              {/* Static nodes can also "Jump" to an existing node — but the ROOT node cannot. */}
              {!isRootNode && (
              <div className="tpl-field">
                <div className="ivr-seg-head">
                  <label className="tpl-field-label">{t.ivrJumpSectionLbl || 'Jump'}</label>
                  <div className="ivr-seg-actions">
                    <button type="button" className="ivr-seg-add-link" disabled={draftHasGoto || draftHasContent} onClick={() => addSeg({ kind: 'goto', target: '' })} title={draftHasGoto ? (t.ivrAddGotoUsed || 'Only one jump is allowed per node') : draftHasContent ? (t.ivrContentExcludesJump || 'Remove the voice first — a node is either a message or a jump') : (t.ivrAddGotoHint || 'Jump the caller to an existing node')}>{IcPlusSm} {t.ivrAddGoto || 'Jump'}</button>
                  </div>
                </div>
                {(draft.segments || []).map((seg, idx) => seg.kind === 'goto' ? (
                  <div key={idx} className="ivr-seg-builder">
                    <div className="ivr-seg-edit ivr-seg-edit-goto">
                      <IvrSelect value={seg.target} onChange={(v) => updateSeg(idx, { target: v })} placeholder={t.ivrGotoPh || 'Go to node…'} options={jumpTargetOptions} emptyLabel={t.ivrJumpNoTargets || 'No other nodes yet — add one to jump to it.'} t={t} />
                      <button type="button" className="ivr-seg-remove" title={t.delete || 'Remove'} onClick={() => removeSeg(idx)}>{IvrIcTrash}</button>
                    </div>
                  </div>
                ) : null)}
              </div>
              )}
            </>
          )}

          <div className="tpl-field">
            <label className="tpl-field-label">{t.ivrTimeoutLbl || 'Timeout'}</label>
            <div className="ivr-timeout-row">
              <input className="tpl-field-input" inputMode="numeric" value={draft.timeout} placeholder="0" onChange={(e) => setDraft({ ...draft, timeout: e.target.value.replace(/[^\d]/g, '').slice(0, 3) })} />
              <span className="ivr-timeout-unit">{t.ivrSec || 'sec'}</span>
            </div>
          </div>

          {showKeypad && (
            <div className="tpl-field ivr-keypad-field">
              <label className="tpl-field-label">{t.ivrKeyLbl || 'Option to move you here'}</label>
              <div className="ivr-keypad" role="group" aria-label={t.ivrKeyLbl || 'Option to move you here'}>
                {['1','2','3','4','5','6','7','8','9','*','0','#'].map(k => {
                  const isSel = (draft.keys || []).includes(k);
                  const isTaken = takenKeys.has(k) && !isSel;
                  return (
                    <button
                      key={k}
                      type="button"
                      className={`ivr-keypad-key ${isSel ? 'is-selected' : ''} ${isTaken ? 'is-taken' : ''}`}
                      disabled={isTaken}
                      title={isTaken ? (t.ivrKeyTaken || 'Already used by another option') : (t.ivrKeySet || 'Use this keypad number')}
                      onClick={() => setDraft(d => ({ ...d, keys: (d.keys || []).includes(k) ? (d.keys || []).filter(x => x !== k) : [...(d.keys || []), k] }))}
                    >{k}</button>
                  );
                })}
              </div>
              <span className="ivr-help">{t.ivrKeyHint || 'The keypad number(s) the caller presses to reach this node — tap to select one or more.'}</span>
            </div>
          )}

          {!readOnly && (
          <div className="ivr-create-panel-foot">
            {editId && <button type="button" className="ivr-cpanel-del" onClick={() => requestDelete(editId)}>{t.delete || 'Delete'}</button>}
            <button type="button" className="ivr-cpanel-save" disabled={!canSave} onClick={saveNode}>{(editId || isDyn) ? (t.save || 'Save') : (t.add || 'Add')}</button>
          </div>
          )}
          </fieldset>) : (
            <div className="ivr-cpanel-vars">
              <div className="ivr-rev-vars-head"><span>{t.ivrVariablesHead || 'Variables'}</span><span className="ivr-rev-vars-hint">{t.ivrVarsExampleHint || 'add an example value'}</span></div>
              {(() => {
                // Only the variables actually USED in the flow (any node) or in the node being
                // edited — each with an editable EXAMPLE value the caller will hear in preview.
                const usedKeys = new Set([...ivr.nodes, { segments: draft.segments }]
                  .flatMap(n => (n.segments || []).filter(s => s.kind === 'variable' && s.key).map(s => s.key)));
                const shown = (ivr.variables || []).filter(v => usedKeys.has(v.key));
                return shown.length === 0
                  ? <div className="ivr-empty">{t.ivrNoVarsUsed || 'No variables yet — add a variable to a node to set its example here.'}</div>
                  : shown.map(v => (
                      <div key={v.key} className="ivr-rev-var">
                        <div className="ivr-rev-var-top">
                          <span className="ivr-rev-var-key" title={'{' + v.key + '}'}>{'{' + v.key + '}'}</span>
                          <span className="ivr-rev-var-type">{ivrVarTypeLabel(v.type)}</span>
                        </div>
                        <input type="text" className="ivr-rev-var-input" value={v.sample == null ? '' : v.sample} placeholder={t.ivrVarExample || 'add an example value'} onChange={(e) => updateIvr({ variables: (ivr.variables || []).map(x => x.key === v.key ? { ...x, sample: e.target.value } : x) })} />
                      </div>
                    ));
              })()}
            </div>
          )}
        </div>
      )}

      {/* Dynamic: add-variable modal */}
      {varModalOpen && (
        <IvrVariableModal existingKeys={(ivr.variables || []).map(v => v.key)} onClose={() => { setVarModalOpen(false); setVarModalSegIdx(null); }} onAdd={addVariable} t={t} />
      )}

      {/* Confirm before deleting a node (and its subtree) */}
      {confirmDel != null && (() => {
        const node = ivr.nodes.find(n => n.id === confirmDel);
        const kids = subtreeIds(confirmDel).size - 1;
        return (
          <div className="ivr-modal-backdrop" onMouseDown={() => setConfirmDel(null)}>
            <div className="ivr-modal ivr-confirm" onMouseDown={(e) => e.stopPropagation()}>
              <div className="ivr-modal-head">
                <h4>{t.ivrDeleteTitle || 'Delete node?'}</h4>
                <button type="button" className="ivr-icon-btn" onClick={() => setConfirmDel(null)}>{IvrIcClose}</button>
              </div>
              <div className="ivr-modal-body">
                <p className="ivr-confirm-text">
                  {t.ivrDeleteConfirm || 'Are you sure you want to delete'} <strong>{node && node.label ? `"${node.label}"` : (t.ivrThisNode || 'this node')}</strong>?
                  {kids > 0 ? ` This also removes its ${kids} sub-node${kids > 1 ? 's' : ''}.` : ''}
                  {' '}{t.ivrDeleteUndo || 'This action cannot be undone.'}
                </p>
              </div>
              <div className="ivr-modal-foot">
                <button type="button" className="btn btn-secondary" onClick={() => setConfirmDel(null)}>{t.cancel || 'Cancel'}</button>
                <button type="button" className="btn ivr-btn-danger" onClick={() => doDeleteNode(confirmDel)}>{t.delete || 'Delete'}</button>
              </div>
            </div>
          </div>
        );
      })()}
    </div>
  );
};

// ------------------------------ Variable creation modal ------------------------------

const IvrVariableModal = ({ existingKeys, onClose, onAdd, t }) => {
  const [key, setKey] = useStateIvr('');
  const [type, setType] = useStateIvr('number');
  const [sample, setSample] = useStateIvr('');
  const valid = /^[a-z][a-z0-9_]{1,31}$/.test(key) && !existingKeys.includes(key) && sample.trim().length > 0;
  return (
    <div className="ivr-modal-backdrop" onMouseDown={onClose}>
      <div className="ivr-modal" onMouseDown={(e) => e.stopPropagation()}>
        <div className="ivr-modal-head">
          <h4>{t.ivrAddVariableTitle || 'Add variable'}</h4>
          <button type="button" className="ivr-icon-btn" onClick={onClose}><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
        </div>
        <div className="ivr-modal-body">
          <div className="tpl-field">
            <label className="tpl-field-label">{t.ivrVarKey || 'Variable key'}</label>
            <input className="tpl-field-input" placeholder="e.g. otp_code" value={key} onChange={(e) => setKey(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))} />
            <span className="ivr-help">{t.ivrVarKeyHint || 'Lower-case letters, digits and underscore. Starts with a letter.'}</span>
          </div>
          <div className="tpl-field">
            <label className="tpl-field-label">{t.ivrVarType || 'Type'}</label>
            <IvrSelect value={type} onChange={setType} options={[{ value: 'number', label: t.ivrVarNumber || 'Number' }, { value: 'digit', label: t.ivrVarDigit || 'Digit' }, { value: 'date', label: t.ivrVarDate || 'Date' }, { value: 'time', label: t.ivrVarTime || 'Time' }, { value: 'string', label: t.ivrVarString || 'String' }]} t={t} />
          </div>
          <div className="tpl-field">
            <label className="tpl-field-label">{t.ivrVarSample || 'Sample value (for preview)'}</label>
            <input className="tpl-field-input" value={sample} onChange={(e) => setSample(e.target.value)} placeholder={type === 'digit' ? '123456' : type === 'number' ? '1500.50' : type === 'string' ? 'Ahmed Ali' : type === 'date' ? '2026-03-05' : '14:30'} />
          </div>
        </div>
        <div className="ivr-modal-foot">
          <button type="button" className="btn btn-secondary" onClick={onClose}>{t.cancel}</button>
          <button type="button" className="btn btn-primary" disabled={!valid} onClick={() => onAdd({ key, type, sample })}>{t.add || 'Add'}</button>
        </div>
      </div>
    </div>
  );
};

// ------------------------------ Step 3: Share & Submit ------------------------------

const TplIvrStep3 = ({ data, setData, t }) => {
  // Identical "Share & Submit" experience as the WhatsApp template wizard
  // (Shared With picker + Selected Users). Reused via the window global so the
  // two flows stay in lockstep.
  const Share = window.TplWizardStep3;
  return Share ? <Share data={data} setData={setData} t={t} /> : null;
};

// ------------------------------ Wizard wrapper ------------------------------

const TplIvrWizard = ({ t, lang, step, data, setData }) => {
  // Ensure ivr scaffolding exists once
  useEffectIvr(() => {
    if (!data.ivr) setData({ ...data, ivr: defaultIvrData('static') });
    // eslint-disable-next-line
  }, []);

  return (
    <div className="tpl-wizard ivr-wizard ac-card">
      <div className="ac-card-head">
        <div className="ac-card-title">{t.ivrWizardTitle || 'Create IVR Voice Template'}</div>
        <div className="ac-step-counter">{(t.stepNum || 'step').toLowerCase()} <strong>{step}/3</strong></div>
      </div>

      <TplStepper step={step} t={t} />

      <div className="ac-card-body tpl-wizard-body">
        {step === 1 && <TplIvrStep1 data={data} setData={setData} t={t} />}
        {step === 2 && <TplIvrStep2 data={data} setData={setData} t={t} />}
        {step === 3 && <TplIvrStep3 data={data} setData={setData} t={t} />}
      </div>
    </div>
  );
};

// ------------------------------ Right-rail IVR preview (phone + DTMF) ------------------------------

const TplIvrPreview = ({ tpl, t }) => {
  if (!tpl || !tpl.ivr) {
    return (
      <div className="ivr-preview-empty">
        <div className="ivr-phone-frame">
          <div className="ivr-phone-screen">
            <div className="ivr-phone-empty-text">{t.ivrPreviewEmpty || 'Build a node and add a recording to start the preview.'}</div>
          </div>
        </div>
      </div>
    );
  }
  const { ivr } = tpl;
  const nodesMap = useMemoIvr(() => Object.fromEntries(ivr.nodes.map(n => [n.id, n])), [ivr.nodes]);
  const byParent = useMemoIvr(() => buildIvrTree(ivr.nodes), [ivr.nodes]);
  const [currentId, setCurrentId] = useStateIvr('n_root');
  const [path, setPath] = useStateIvr(['n_root']);
  const [transcript, setTranscript] = useStateIvr([]);
  const [playing, setPlaying]   = useStateIvr(false);
  const playTimer = useRefIvr(null);

  const current = nodesMap[currentId];

  // Build a readable transcript for a node
  const renderSegment = (seg) => {
    if (seg.kind === 'recording') return `[♪ ${seg.name || 'recording'}]`;
    const v = ivr.variables.find(x => x.key === seg.key);
    if (!v) return `{${seg.key}}`;
    if (v.type === 'digit')  return (v.sample || '').split('').join(' ');
    return v.sample || '';
  };

  const playNode = (id) => {
    if (playTimer.current) clearTimeout(playTimer.current);
    const n = nodesMap[id];
    if (!n) return;
    setCurrentId(id);
    setPath(p => p[p.length - 1] === id ? p : [...p, id]);
    setPlaying(true);
    const lines = (n.segments || []).map(renderSegment);
    setTranscript(lines.length ? lines : ['[silent node]']);
    const dur = Math.max(1500, estimateNodeDuration(n) * 600);
    playTimer.current = setTimeout(() => setPlaying(false), dur);
  };

  useEffectIvr(() => {
    playNode('n_root');
    return () => playTimer.current && clearTimeout(playTimer.current);
    // eslint-disable-next-line
  }, [tpl]);

  const onKeyPress = (k) => {
    if (!current) return;
    const kids = byParent[current.id] || [];
    const match = kids.find(c => c.key === k);
    if (match) { playNode(match.id); return; }
    // No match — replay current
    playNode(current.id);
  };

  const onRestart = () => { setPath(['n_root']); playNode('n_root'); };
  const onBack    = () => {
    if (!current?.parent) return;
    playNode(current.parent);
  };

  return (
    <div className="ivr-phone-wrap">
      <div className="ivr-phone-frame">
        <div className="ivr-phone-notch" />
        <div className="ivr-phone-screen">
          <div className="ivr-phone-status">{t.ivrCallStatus || 'In call · Aramco IVR'}</div>
          <div className="ivr-phone-node">
            <div className="ivr-phone-node-key">{current?.key || '·'}</div>
            <div className="ivr-phone-node-label">{current?.label || ''}</div>
            <div className={`ivr-phone-dot ${playing ? 'playing' : ''}`} />
          </div>
          <div className="ivr-phone-transcript">
            {transcript.map((line, i) => <div key={i} className="ivr-phone-line">{line}</div>)}
          </div>
          <div className="ivr-phone-choices">
            {(byParent[currentId] || []).map(c => (
              <div key={c.id} className="ivr-phone-choice"><span className="k">{c.key}</span><span>{c.label}</span></div>
            ))}
            {(byParent[currentId] || []).length === 0 && (
              <div className="ivr-phone-choice-empty">{t.ivrTerminalNode || 'Terminal node — no further options'}</div>
            )}
          </div>
        </div>
        <div className="ivr-phone-keypad">
          {['1','2','3','4','5','6','7','8','9','*','0','#'].map(k => (
            <button key={k} type="button" className="ivr-key" onClick={() => onKeyPress(k)}>{k}</button>
          ))}
        </div>
        <div className="ivr-phone-controls">
          <button type="button" className="ivr-ctrl" onClick={onBack} disabled={!current?.parent}>↶ {t.ivrBack || 'Back'}</button>
          <button type="button" className="ivr-ctrl primary" onClick={onRestart}>↻ {t.ivrRestart || 'Restart'}</button>
        </div>
      </div>
    </div>
  );
};

// ------------------------------ Right-rail IVR review (Checker details view) ------------------------------
// Replaces the phone simulator: a structured, read-only view of the whole IVR tree
// so a Checker can review the template and everything created within it — every node,
// the DTMF key that reaches it, its voice/variable content, and its terminal action.

const ivrTermLabel = (terminal, t) => {
  if (!terminal || !terminal.type) return null;
  const map = {
    hangup: t.ivrTermHangup || 'End the call',
    return_parent: t.ivrTermReturn || 'Return to previous menu',
    return_root: t.ivrTermRoot || 'Return to main menu',
    transfer: t.ivrTermTransfer || 'Transfer to an agent',
    repeat: t.ivrTermRepeat || 'Repeat this message',
  };
  return map[terminal.type] || terminal.type;
};

// Recording file name → a human label for review (drop extension/locale suffix).
const ivrPrettyName = (name) => {
  if (!name) return 'Recording';
  const s = String(name).replace(/\.(wav|mp3|m4a|ogg)$/i, '').replace(/[-_](ar|en)$/i, '').replace(/[-_]+/g, ' ').trim();
  return s.charAt(0).toUpperCase() + s.slice(1);
};

// Friendly clip-duration label.
const ivrFmtDur = (s) => `00:${String(Math.max(0, Math.round(s || 8))).padStart(2, '0')}s`;

// Short clip duration for the step rows (e.g. "4s").
const ivrShortDur = (s) => `${Math.max(1, Math.round(s || 8))}s`;
// Spoken read time (seconds) for a variable value — paces the preview playback.
const ivrVarReadDur = (val) => { const s = String(val == null ? '' : val).trim(); return Math.max(1, Math.min(6, s.length * 0.13 + 0.7)); };
// The audible items of a node, in order (recordings + variables; a "go to" carries no audio).
const ivrNodeItems = (n) => {
  const segs = (n.segments && n.segments.length) ? n.segments : (n.voice ? [{ kind: 'recording', name: n.voice, dur: n.dur }] : []);
  const out = [];
  segs.forEach((seg, i) => { if (seg.kind === 'recording' || seg.kind === 'variable') out.push({ key: n.id + '-' + i, nodeId: n.id }); });
  return out;
};
// Depth-first walk of the whole tree (for "simulate full call").
const ivrDfsNodes = (byParent) => { const out = []; const walk = (pid) => (byParent[pid] || []).forEach(n => { out.push(n); walk(n.id); }); walk('__root__'); return out; };

// One node as a collapsible review card: a START / PRESS-key header with a Level badge and
// expand toggle, the numbered list of everything the caller hears (each step individually
// playable + a "Play message" that reads the whole node with the variable values spoken in
// place), and a footer with the keypad-option toggle or terminal action. Children render as
// nested, collapsible cards along a connector rail; indentation is capped for deep trees.
const IvrFlowNode = ({ id, byParent, nodesMap, vars, varValues, depth, isRoot, collapsed, toggleCollapse, playingKey, activeNodeId, paused, playNode, playStep, stop, togglePause, onBarEnd, nodeRefs, dfsIndex, t }) => {
  const n = nodesMap[id];
  if (!n) return null;
  const kids = byParent[id] || [];
  const hasKids = kids.length > 0;
  const isCollapsed = collapsed.has(id);
  const term = ivrTermLabel(n.terminal, t);
  const segs = (n.segments && n.segments.length) ? n.segments : (n.voice ? [{ kind: 'recording', name: n.voice, dur: n.dur }] : []);
  const keys = ivrKeysLabel(ivrNodeKeys(n));
  const hasAudio = segs.some(s => s.kind === 'recording' || s.kind === 'variable');
  const isNodePlaying = activeNodeId === id; // any sound currently coming from this node
  // A node may route the caller onward with one "goto" — shown as a pill in the caption row.
  const gotoSeg = segs.find(s => s.kind === 'goto');
  const gotoTgt = gotoSeg ? (((nodesMap[gotoSeg.target] || {}).label) || (t.ivrNode || 'node')) : '';
  // "Return to" only when routing back to a start/root node (e.g. Main Menu); "Jump to" otherwise.
  const gotoTgtParent = gotoSeg ? (nodesMap[gotoSeg.target] || {}).parent : 'x';
  const gotoBack = gotoSeg ? (!gotoTgtParent || gotoTgtParent === '__root__') : false;
  const gotoLbl = gotoBack ? (t.ivrReturnTo || 'Return to') : (t.ivrJumpTo || 'Jump to');
  return (
    <div className="ivr-vbranch">
      <div className={`ivr-vnode ${isRoot ? 'is-root' : 'is-child'} ${activeNodeId === id ? 'is-active' : ''}`} ref={el => { if (nodeRefs && nodeRefs.current) nodeRefs.current[id] = el; }}>
        <div className="ivr-vnode-head">
          {hasKids
            ? <button type="button" className={`ivr-vnode-twist ${isCollapsed ? 'is-collapsed' : ''}`} title={isCollapsed ? (t.ivrExpand || 'Expand') : (t.ivrCollapse || 'Collapse')} aria-label={isCollapsed ? (t.ivrExpand || 'Expand') : (t.ivrCollapse || 'Collapse')} onClick={() => toggleCollapse(id)}>{IcChevRightTree}</button>
            : <span className="ivr-vnode-twist is-leaf" aria-hidden="true" />}
          {isRoot
            ? <span className="ivr-vnode-tag is-start">{IcPlaySm}{t.ivrStart || 'Start'}</span>
            : <span className="ivr-vnode-tag is-press"><span className="ivr-vnode-key">{keys || '—'}</span>{t.ivrPressLbl || 'Press'}</span>}
          <span className="ivr-vnode-name" title={n.label}>{n.label || (t.ivrUntitled || 'Untitled')}</span>
          <span className="ivr-vnode-lvl" title={(t.ivrLevelWord || 'Level') + ' ' + (depth + 1)}>{(t.ivrLevelAbbr || 'L')}{depth + 1}</span>
        </div>
        {(hasAudio || gotoSeg) && (
        <div className="ivr-vnode-caption">
          {gotoSeg && (
            <span className="ivr-vnode-jump" title={gotoLbl + ' ' + gotoTgt}>
              <span className="ivr-vnode-jump-ic" aria-hidden="true">{IcJump}</span>
              <span className="ivr-vnode-jump-lbl">{gotoLbl}</span>
              <span className="ivr-vnode-jump-tgt">{gotoTgt}</span>
            </span>
          )}
          {hasAudio && <button type="button" className={`ivr-vnode-playmsg ${isNodePlaying && !paused ? 'is-playing' : ''}`} title={isNodePlaying ? (paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.ivrPlayMsgHint || 'Play this whole message')} onClick={() => isNodePlaying ? togglePause() : playNode(id)}>{isNodePlaying && !paused ? IcPauseSm : IcPlaySm}{isNodePlaying ? (paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.ivrPlayMsg || 'Play')}</button>}
        </div>
        )}
        {segs.length === 0
          ? <div className="ivr-vnode-empty">{t.ivrNoMessage || 'No message set'}</div>
          : (
            <ol className="ivr-vsteps">
              {segs.map((seg, si) => {
                if (seg.kind === 'goto') return null; // routing is shown as a pill in the caption row
                const no = segs.slice(0, si).filter(s => s.kind !== 'goto').length + 1;
                const stepKey = id + '-' + si;
                const isP = playingKey === stepKey;
                if (seg.kind === 'recording') {
                  return (
                    <li key={si} className={`ivr-vstep is-rec ${isP ? 'is-playing' : ''}`}>
                      <span className="ivr-vstep-no">{no}</span>
                      <button type="button" className="ivr-vstep-play" title={isP ? (paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.play || 'Play')} onClick={() => playStep(id, si)}>{isP && !paused ? IcPauseSm : IcPlaySm}</button>
                      <span className="ivr-vstep-name" title={seg.name}>{ivrPrettyName(seg.name)}</span>
                      <span className="ivr-vstep-dur">{ivrShortDur(seg.dur)}</span>
                      {isP && <span className="ivr-vstep-bar" style={{ animationDuration: (seg.dur || 8) + 's', animationPlayState: paused ? 'paused' : 'running' }} onAnimationEnd={onBarEnd} />}
                    </li>
                  );
                }
                const vmeta = vars.find(v => v.key === seg.key) || {};
                const val = varValues[seg.key];
                return (
                  <li key={si} className={`ivr-vstep is-var ${isP ? 'is-playing' : ''}`}>
                    <span className="ivr-vstep-no">{no}</span>
                    <span className="ivr-vstep-var">{'{' + seg.key + '}'}</span>
                    {val
                      ? <span className="ivr-vstep-val" title={(vmeta.type ? ivrVarTypeLabel(vmeta.type) + ' · ' : '') + val}>{val}</span>
                      : <span className="ivr-vstep-type">{ivrVarTypeLabel(vmeta.type)}</span>}
                    {isP && <span className="ivr-vstep-bar is-var" style={{ animationDuration: ivrVarReadDur(val) + 's', animationPlayState: paused ? 'paused' : 'running' }} onAnimationEnd={onBarEnd} />}
                  </li>
                );
              })}
            </ol>
          )}
        {hasKids
          ? <button type="button" className={`ivr-vnode-foot ivr-vnode-opts ${isCollapsed ? 'is-collapsed' : ''}`} onClick={() => toggleCollapse(id)}>
              <span className="ivr-vnode-opts-n">{kids.length} {kids.length === 1 ? (t.ivrKeypadOption || 'keypad option') : (t.ivrKeypadOptions || 'keypad options')}</span>
              <span className="ivr-vnode-opts-x">{isCollapsed ? (t.ivrShowOpts || 'show') : (t.ivrHideOpts || 'hide')}{IcChevRightTree}</span>
            </button>
          : term ? <div className="ivr-vnode-foot"><span className="ivr-vnode-term">↳ {term}</span></div> : null}
      </div>
      {hasKids && !isCollapsed && (
        <div className="ivr-vkids">
          {kids.map(k => <IvrFlowNode key={k.id} id={k.id} byParent={byParent} nodesMap={nodesMap} vars={vars} varValues={varValues} depth={depth + 1} isRoot={false} collapsed={collapsed} toggleCollapse={toggleCollapse} playingKey={playingKey} activeNodeId={activeNodeId} paused={paused} playNode={playNode} playStep={playStep} stop={stop} togglePause={togglePause} onBarEnd={onBarEnd} nodeRefs={nodeRefs} dfsIndex={dfsIndex} t={t} />)}
        </div>
      )}
    </div>
  );
};

const TplIvrDetails = ({ tpl, t }) => {
  const ivr = tpl && tpl.ivr;
  const nodes = (ivr && ivr.nodes) || [];
  const vars = (ivr && ivr.variables) || [];

  const nodesMap = useMemoIvr(() => Object.fromEntries(nodes.map(n => [n.id, n])), [nodes]);
  const byParent = useMemoIvr(() => buildIvrTree(nodes), [nodes]);
  const parentOf = useMemoIvr(() => Object.fromEntries(nodes.map(n => [n.id, n.parent || null])), [nodes]);
  const depthOf = useMemoIvr(() => { const d = {}; const walk = (pid, dep) => (byParent[pid] || []).forEach(nn => { d[nn.id] = dep; walk(nn.id, dep + 1); }); walk('__root__', 0); return d; }, [byParent]);
  // Visual (DFS pre-order) position of each node, so a jump's arrow can point toward where the target renders (above/below).
  const dfsIndex = useMemoIvr(() => { const m = {}; ivrDfsNodes(byParent).forEach((nn, i) => { m[nn.id] = i; }); return m; }, [byParent]);

  // Editable variable values — used both inline and spoken during preview playback.
  const [varValues, setVarValues] = useStateIvr(() => Object.fromEntries(vars.map(v => [v.key, v.sample || ''])));

  // Collapse state. Large trees start with only the first level open so 10+ levels stay tidy.
  const [collapsed, setCollapsed] = useStateIvr(() => {
    if (nodes.length <= 6) return new Set();
    return new Set(nodes.filter(n => (depthOf[n.id] || 0) >= 1 && (byParent[n.id] || []).length > 0).map(n => n.id));
  });
  const toggleCollapse = (id) => setCollapsed(prev => { const s = new Set(prev); if (s.has(id)) s.delete(id); else s.add(id); return s; });
  const expandAll = () => setCollapsed(new Set());
  const collapseAll = () => setCollapsed(new Set(nodes.filter(n => (byParent[n.id] || []).length > 0).map(n => n.id)));

  // Playback sequencer: a queue of audible items advanced by each step's progress-bar end,
  // so per-step, per-node and full-call playback all reuse one moving highlight.
  const [seq, setSeq] = useStateIvr([]);
  const [seqIdx, setSeqIdx] = useStateIvr(0);
  const [paused, setPaused] = useStateIvr(false); // freeze the active step's progress bar in place; resume continues from where it stopped
  const nodeRefs = useRefIvr({});
  const [tab, setTab] = useStateIvr('levels');
  const active = (seq.length && seqIdx < seq.length) ? seq[seqIdx] : null;
  const playingKey = active ? active.key : null;
  const activeNodeId = active ? active.nodeId : null;
  const isPlaying = seq.length > 0;
  const stop = () => { setSeq([]); setSeqIdx(0); setPaused(false); };
  const onBarEnd = () => setSeqIdx(i => i + 1);
  const togglePause = () => setPaused(p => !p);
  const playNode = (id) => { const items = ivrNodeItems(nodesMap[id]); if (!items.length) return; setPaused(false); setSeq(items); setSeqIdx(0); };
  const playStep = (id, i) => { const key = id + '-' + i; if (playingKey === key) { togglePause(); return; } setPaused(false); setSeq([{ key, nodeId: id }]); setSeqIdx(0); };
  const playFullCall = () => { const items = ivrDfsNodes(byParent).flatMap(n => ivrNodeItems(n)); if (!items.length) return; setPaused(false); setCollapsed(new Set()); setSeq(items); setSeqIdx(0); };

  // Advancement is driven by the active step's own progress bar: its onAnimationEnd calls
  // onBarEnd() to move to the next item. Pausing sets the bar's animation-play-state to
  // "paused", which freezes it where it is AND keeps it from ever reaching its end, so the
  // queue cannot advance while paused; resuming continues the bar from the frozen position.
  // Stop clears the queue (unmounts the bar), so playback can never advance on its own.

  // End of queue → reset.
  useEffectIvr(() => { if (seq.length && seqIdx >= seq.length) { setSeq([]); setSeqIdx(0); } }, [seqIdx, seq]);
  // Follow the playing step: open its ancestors and scroll it into view.
  useEffectIvr(() => {
    if (!playingKey || !activeNodeId) return;
    setCollapsed(prev => { let next = null; let p = parentOf[activeNodeId]; while (p) { if (prev.has(p)) { if (!next) next = new Set(prev); next.delete(p); } p = parentOf[p]; } return next || prev; });
    const el = nodeRefs.current && nodeRefs.current[activeNodeId];
    if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
  }, [playingKey]);

  // ---- Orange "jump" connectors: EACH goto gets its OWN vertical lane inside a dedicated left
  // channel (the flow list's padding-left). Because every lane sits left of every card, no two
  // jump lines overlap and none ever crosses a card — each line runs from its source out to its
  // lane, up/down, then into its target card. ----
  const JUMP_LANE0 = 6, JUMP_LANE_GAP = 12, MAX_JUMP_LANES = 6;
  const gotoCount = useMemoIvr(() => nodes.filter(n => (n.segments || []).some(s => s.kind === 'goto' && s.target)).length, [nodes]);
  const jumpChannel = gotoCount > 0 ? (JUMP_LANE0 + Math.min(gotoCount, MAX_JUMP_LANES) * JUMP_LANE_GAP) : 0;
  const listRef = useRefIvr(null);
  const [jumpPaths, setJumpPaths] = useStateIvr([]);
  const measureJumps = () => {
    const list = listRef.current;
    if (!list) return;
    const base = list.getBoundingClientRect();
    const out = [];
    let lane = 0;                             // a fresh lane per rendered jump → separate lines
    nodes.forEach(n => {
      const g = (n.segments || []).find(s => s.kind === 'goto' && s.target);
      if (!g) return;
      const sEl = nodeRefs.current[n.id];
      if (!sEl) return;                       // source not rendered (an ancestor is collapsed)
      const s = sEl.getBoundingClientRect();
      const sLeft = s.left - base.left, sTop = s.top - base.top;
      const sY = sTop + 52;                   // anchor at the "Jump to" pill row
      const tEl = nodeRefs.current[g.target];
      let d, arrow;
      if (tEl) {                              // target rendered → its own lane, connect into the card
        const tr = tEl.getBoundingClientRect();
        const tLeft = tr.left - base.left, tTop = tr.top - base.top;
        const up = (tTop + tr.height / 2) < sTop;
        const tY = up ? (tTop + tr.height - 12) : (tTop + 12);
        const gx = JUMP_LANE0 + (lane % MAX_JUMP_LANES) * JUMP_LANE_GAP;   // this jump's own lane
        lane++;
        d = 'M' + sLeft + ' ' + sY + ' H' + gx + ' V' + tY + ' H' + tLeft;
        // arrowhead sits ON the target node's left edge, pointing right INTO the card
        arrow = 'M' + (tLeft - 7) + ' ' + (tY - 5) + ' L' + tLeft + ' ' + tY + ' L' + (tLeft - 7) + ' ' + (tY + 5);
      } else {                                // target collapsed → short directional arrow in the gutter
        const ti = dfsIndex[g.target], si = dfsIndex[n.id];
        const up = (typeof ti === 'number' && typeof si === 'number') ? (ti < si) : true;
        const gx = Math.max(6, sLeft - 14);
        const endY = up ? (sY - 92) : (sY + 92);
        d = 'M' + sLeft + ' ' + sY + ' H' + gx + ' V' + endY;
        arrow = up ? ('M' + (gx - 5) + ' ' + (endY + 7) + ' L' + gx + ' ' + endY + ' L' + (gx + 5) + ' ' + (endY + 7))
                   : ('M' + (gx - 5) + ' ' + (endY - 7) + ' L' + gx + ' ' + endY + ' L' + (gx + 5) + ' ' + (endY - 7));
      }
      out.push({ key: n.id, d, arrow });
    });
    setJumpPaths(out);
  };
  const measureRef = useRefIvr(measureJumps);
  measureRef.current = measureJumps;
  useLayoutEffectIvr(() => { measureRef.current(); }, [collapsed, byParent, nodes, tab]);
  useEffectIvr(() => {
    const list = listRef.current;
    if (!list || typeof ResizeObserver === 'undefined') return undefined;
    const ro = new ResizeObserver(() => { if (measureRef.current) measureRef.current(); });
    ro.observe(list);
    return () => ro.disconnect();
  }, []);

  if (!ivr || !nodes.length) {
    return <div className="ivr-rev-empty">{t.ivrReviewEmpty || 'No IVR voice flow to display.'}</div>;
  }
  const isDyn = ivr.type === 'dynamic';
  const roots = byParent['__root__'] || [];
  const recCount = nodes.reduce((a, n) => a + ((n.segments || []).filter(s => s.kind === 'recording').length || (n.voice ? 1 : 0)), 0);
  const deep = (Math.max(0, ...Object.values(depthOf)) + 1);
  const hasVars = isDyn && vars.length > 0;
  const tabs = [
    { id: 'info', label: t.ivrTabInfo || 'Information' },
    { id: 'levels', label: t.ivrTabLevels || 'Levels' },
    ...(hasVars ? [{ id: 'variables', label: t.ivrTabVariables || 'Variables' }] : []),
  ];
  const activeTab = (tab === 'variables' && !hasVars) ? 'levels' : tab;
  return (
    <div className="ivr-flow">
      <div className="ivr-rev-top">
        <div className="ivr-rev-title">{tpl.name || (t.ivrReviewTitle || 'IVR Voice flow')}</div>
        <span className={`ivr-rev-badge ${isDyn ? 'dyn' : 'stat'}`}>{isDyn ? (t.ivrBadgeDynamic || 'Dynamic') : (t.ivrBadgeStatic || 'Static')}</span>
      </div>

      <div className="ivr-tabs" role="tablist">
        {tabs.map(tb => (
          <button key={tb.id} type="button" role="tab" aria-selected={activeTab === tb.id} className={`ivr-tab ${activeTab === tb.id ? 'is-active' : ''}`} onClick={() => setTab(tb.id)}>{tb.label}</button>
        ))}
      </div>

      {activeTab === 'info' && (
        <div className="ivr-tabpanel">
          <div className="ivr-info-grid">
            <div className="ivr-info-row"><span className="ivr-info-k">{t.ivrInfoNodes || 'Nodes'}</span><span className="ivr-info-v">{nodes.length}</span></div>
            <div className="ivr-info-row"><span className="ivr-info-k">{t.ivrInfoRecordings || 'Recordings'}</span><span className="ivr-info-v">{recCount}</span></div>
            {isDyn && <div className="ivr-info-row"><span className="ivr-info-k">{t.ivrInfoVariables || 'Variables'}</span><span className="ivr-info-v">{vars.length}</span></div>}
            <div className="ivr-info-row"><span className="ivr-info-k">{t.ivrInfoLevels || 'Levels'}</span><span className="ivr-info-v">{deep}</span></div>
            <div className="ivr-info-row"><span className="ivr-info-k">{t.ivrInfoType || 'Type'}</span><span className="ivr-info-v">{isDyn ? (t.ivrBadgeDynamic || 'Dynamic') : (t.ivrBadgeStatic || 'Static')}</span></div>
            {tpl.referenceId && <div className="ivr-info-row"><span className="ivr-info-k">{t.ivrInfoRef || 'Reference ID'}</span><span className="ivr-info-v">{tpl.referenceId}</span></div>}
          </div>
          {tpl.body && <p className="ivr-info-desc">{tpl.body}</p>}
        </div>
      )}

      {activeTab === 'levels' && (
        <div className="ivr-tabpanel ivr-tab-levels">
          <div className="ivr-flow-toolbar">
            <button type="button" className={`ivr-sim-btn ${isPlaying && !paused ? 'is-playing' : ''}`} onClick={isPlaying ? togglePause : playFullCall} title={isPlaying ? (paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.ivrSimHint || 'Play the whole flow with the chosen variable values')}>
              {isPlaying ? (paused ? IcPlaySm : IcPauseSm) : IcPlaySm}{isPlaying ? (paused ? (t.ivrResume || 'Resume') : (t.pause || 'Pause')) : (t.ivrSimCall || 'Simulate full call')}
            </button>
            {isPlaying && <button type="button" className="ivr-sim-stop" onClick={stop} title={t.ivrStopPlayback || 'Stop'} aria-label={t.ivrStopPlayback || 'Stop'}>{IcStopSm}</button>}
            {isPlaying && activeNodeId && <span className="ivr-sim-now" title={(nodesMap[activeNodeId] || {}).label}>{(nodesMap[activeNodeId] || {}).label}</span>}
            {!isPlaying && (
              <div className="ivr-flow-tools">
                <button type="button" className="ivr-flow-tool" onClick={expandAll} title={t.ivrExpandAllHint || 'Expand every level'} aria-label={t.ivrExpandAll || 'Expand all'}>{IcExpandAll}<span>{t.ivrExpandAll || 'Expand all'}</span></button>
                <button type="button" className="ivr-flow-tool" onClick={collapseAll} title={t.ivrCollapseAllHint || 'Collapse every level'} aria-label={t.ivrCollapseAll || 'Collapse all'}>{IcCollapseAll}<span>{t.ivrCollapseAll || 'Collapse all'}</span></button>
              </div>
            )}
          </div>
          <div className="ivr-levels-scroll">
            <div className="ivr-flow-list" ref={listRef} style={{ paddingLeft: jumpChannel ? jumpChannel + 'px' : undefined }}>
              <svg className="ivr-jump-layer" aria-hidden="true">
                {jumpPaths.map(p => (
                  <g key={p.key}>
                    <path className="ivr-jump-line" d={p.d} />
                    <path className="ivr-jump-arrow" d={p.arrow} />
                  </g>
                ))}
              </svg>
              {roots.map(r => <IvrFlowNode key={r.id} id={r.id} byParent={byParent} nodesMap={nodesMap} vars={vars} varValues={varValues} depth={0} isRoot={true} collapsed={collapsed} toggleCollapse={toggleCollapse} playingKey={playingKey} activeNodeId={activeNodeId} paused={paused} playNode={playNode} playStep={playStep} stop={stop} togglePause={togglePause} onBarEnd={onBarEnd} nodeRefs={nodeRefs} dfsIndex={dfsIndex} t={t} />)}
            </div>
          </div>
        </div>
      )}

      {activeTab === 'variables' && hasVars && (
        <div className="ivr-tabpanel">
          <div className="ivr-rev-vars">
            <div className="ivr-rev-vars-head"><span>{t.ivrVariablesHead || 'Variables'}</span><span className="ivr-rev-vars-hint">{t.ivrVarsHint || 'set values to preview'}</span></div>
            {vars.map(v => (
              <div key={v.key} className="ivr-rev-var">
                <div className="ivr-rev-var-top">
                  <span className="ivr-rev-var-key" title={'{' + v.key + '}'}>{'{' + v.key + '}'}</span>
                  <span className="ivr-rev-var-type">{ivrVarTypeLabel(v.type)}</span>
                </div>
                <input type="text" className="ivr-rev-var-input" value={varValues[v.key] == null ? '' : varValues[v.key]} placeholder={v.sample || (t.ivrEnterValue || 'value')} onChange={e => setVarValues(p => ({ ...p, [v.key]: e.target.value }))} />
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// ===== Single-page Edit / Share screen for IVR voice templates =====
// Reachable from the templates-list row menu. EDIT: the template details (Name, Reference,
// Language) AND the canvas tree are all editable — only Template ID and Service Type are
// locked. SHARE (readOnly): every detail is read-only and the flow is shown as the read-only
// review tree (IVR Voice Flow); only "Shared With" can be changed (Select-all + multi-select,
// matching the WhatsApp share screen).
const TplIvrEditForm = ({ data, setData, t, lang, readOnly = false }) => {
  const ivr = data.ivr || defaultIvrData('static');
  const isDyn = ivr.type === 'dynamic';
  const ShareMulti = window.TplShareMulti;
  const ActionHistory = window.TplActionHistory;
  const serviceTypeLabel = isDyn ? (t.ivrBadgeDynamic || 'Dynamic') : (t.ivrBadgeStatic || 'Static');
  const statusLabel = data.status ? (data.status.charAt(0).toUpperCase() + data.status.slice(1)) : '—';
  // Always-read-only display field (the Template ID, Service Type and the system fields) —
  // same locked look as the WhatsApp edit screen.
  const Locked = ({ label, value, title, full }) => (
    <div className={`tpl-field ${full ? 'full' : ''}`}>
      <label className="tpl-field-label">{label}</label>
      <input className="tpl-field-input is-locked" value={value == null ? '' : value} readOnly disabled title={title || ''} />
    </div>
  );
  // User-editable field — editable in Edit mode, read-only (locked) in Share mode.
  const EditText = ({ label, value, onChange, placeholder }) => (
    <div className="tpl-field">
      <label className="tpl-field-label">{label}</label>
      {readOnly
        ? <input className="tpl-field-input is-locked" value={value == null ? '' : value} readOnly disabled />
        : <input className="tpl-field-input" value={value == null ? '' : value} onChange={onChange} placeholder={placeholder || ''} />}
    </div>
  );
  return (
    <div className="tpl-wizard ac-card tpl-edit-card">
      <div className="ac-card-head">
        <div className="ac-card-title">{readOnly ? (t.shareTemplate || 'Share Template') : (t.editTemplate || 'Edit Template')}</div>
        <span className={`ivr-rev-badge ${isDyn ? 'dyn' : 'stat'}`} style={{ marginInlineStart: 'auto' }}>{serviceTypeLabel}</span>
      </div>
      <div className={`ac-card-body tpl-wizard-body ${readOnly ? 'tpl-share-locked' : ''}`}>
        {readOnly && (
          <div className="tpl-share-note">
            <span>{t.shareNote || 'All fields are read-only. Only "Shared With" can be changed below.'}</span>
          </div>
        )}
        {/* The SAME fields as the Template Details card. Editable where it makes sense
            (Name, Reference ID, Shared With); Template ID, Service Type and the system fields
            (Created By / Creation Date / Channel / Status) stay locked. In Share mode every
            field except "Shared With" is read-only. */}
        <div className="tpl-edit-grid">
          <EditText label={t.detailTemplateName || 'Template Name'} value={data.name} placeholder={t.templateNamePh || ''} onChange={(e) => setData({ ...data, name: e.target.value.slice(0, 512) })} />
          <Locked label={t.detailTemplateId || 'Template ID'} value={`#${data._editId || ''}`} title={t.templateIdLocked || 'Template ID cannot be changed'} />
          <EditText label={t.detailReferenceId || 'Reference ID'} value={data.referenceId} onChange={(e) => setData({ ...data, referenceId: e.target.value })} />
          <Locked label={t.detailCreatedBy || 'Created By'} value={data.createdBy || '—'} />
          <Locked label={t.detailCreationDate || 'Creation Date'} value={tplFmtDate(data.creationDate) + (data.creationTime ? ' • ' + data.creationTime : '')} />
          <Locked label={t.detailCommchannelType || 'Channel'} value={t.ivrChannelName || 'IVR Voice'} />
          <Locked label={t.detailServicesType || 'Service Type'} value={serviceTypeLabel} title={t.serviceTypeLocked || 'Service Type cannot be changed'} />
          <Locked label={t.detailStatus || 'Status'} value={statusLabel} />
          {/* Shared With — editable in BOTH modes (Select all + multi-select) */}
          <div className="tpl-field tpl-edit-shared">
            <label className="tpl-field-label">{t.detailSharedWith || 'Shared With'} <span className="opt">({t.multipleSelect || 'Multiple Select'})</span></label>
            {ShareMulti
              ? <ShareMulti value={data.sharedIds || []} onChange={(ids) => setData({ ...data, sharedIds: ids })} t={t} />
              : <input className="tpl-field-input is-locked" readOnly disabled value={(data.sharedIds || []).length + ' ' + (t.itemsSelected || 'Items Selected')} />}
          </div>
        </div>

        {/* Action history — the same maker–checker table as the More-Details view,
            placed directly under the template details (both Edit and Share). */}
        {ActionHistory && (
          <div className="ivr-edit-history">
            <ActionHistory tpl={data} t={t} />
          </div>
        )}

        {/* Same canvas/template section in both modes. Edit = the live builder; Share = the
            very same canvas, read-only — display + pan/zoom navigation only, no editing. */}
        <div className={`ivr-edit-canvas ${readOnly ? 'is-readonly' : ''}`}>
          <TplIvrStep2 data={data} setData={setData} t={t} readOnly={readOnly} />
        </div>
      </div>
    </div>
  );
};

Object.assign(window, {
  TplIvrWizard,
  TplIvrPreview,
  TplIvrDetails,
  TplIvrEditForm,
  TplIvrStep2,
  defaultIvrData,
  buildIvrTree,
});
