// Nexus Canvas — right-click action factories.
// Pure: each factory takes { ctx } and returns an array of MenuItem
// descriptors consumable by Arbiter.ContextMenu.open.
//
// ctx shape (provided by NexusCanvas at open time):
//   {
//     entityId?, linkId?, pointer:{x,y},        // target
//     state:    { pinned, links, threadName, linkType, mode, selected },
//     actions:  { setPinned, setLinks, setThreadName, setLinkType,
//                 setMode, setSelected, setLinkStart, flash, clearCanvas,
//                 saveThread, loadThread },
//     data:     window.NEXUS_DATA,
//     entityById, nx,
//   }
//
// The factories never reach into DOM or React directly — they mutate state
// via ctx.actions. This keeps the menu declarative and testable.

(function () {
  const LINK_TYPES = ['references', 'contradicts', 'corroborates', 'sent-to',
                      'owns', 'occurred-before', 'authored', 'caused', 'supports'];

  const CARD_W = 160, CARD_H = 72;
  const SEP = { separator: true };

  // ── Helpers ──────────────────────────────────────────────────────────────
  const uniqueLinkId = () => `L-NEW-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;

  const copyToClipboard = (text) => {
    try { navigator.clipboard?.writeText(text); } catch {}
  };

  const neighborsOf = (entityId, links) => {
    const set = new Set();
    links.forEach(l => {
      if (l.from === entityId) set.add(l.to);
      if (l.to === entityId)   set.add(l.from);
    });
    return set;
  };

  const gridLayout = (count, origin = { x: 60, y: 50 }, spacing = { x: 200, y: 120 }) => {
    const cols = Math.min(4, Math.ceil(Math.sqrt(count)));
    return Array.from({ length: count }, (_, i) => ({
      x: origin.x + (i % cols) * spacing.x,
      y: origin.y + Math.floor(i / cols) * spacing.y,
    }));
  };

  const circleLayout = (count, center = { x: 440, y: 260 }, radius = 200) =>
    Array.from({ length: count }, (_, i) => {
      const a = (i / count) * Math.PI * 2 - Math.PI / 2;
      return { x: center.x + Math.cos(a) * radius - CARD_W / 2,
               y: center.y + Math.sin(a) * radius - CARD_H / 2 };
    });

  // Layered by entity type: person top, org below, document below, etc.
  const layeredLayout = (pinned, entityById) => {
    const order = ['person', 'org', 'document', 'event', 'issue', 'artifact'];
    const byType = {};
    pinned.forEach(p => {
      const t = entityById[p.entityId]?.type || 'other';
      (byType[t] = byType[t] || []).push(p);
    });
    const result = [];
    const usedOrder = order.filter(t => byType[t]).concat(
      Object.keys(byType).filter(t => !order.includes(t))
    );
    usedOrder.forEach((t, row) => {
      byType[t].forEach((p, col) => {
        result.push({ entityId: p.entityId, x: 60 + col * 200, y: 40 + row * 120 });
      });
    });
    return result;
  };

  // ── Background menu (empty area) ─────────────────────────────────────────
  function backgroundMenu(ctx) {
    const { state, actions, data, nx, entityById } = ctx;
    const { pinned, links } = state;
    const hasContent = pinned.length > 0;

    const threadSubmenu = data.threads.map(t => ({
      label: t.name,
      hint: `${t.entityIds.length}`,
      onSelect: () => actions.loadThread(t.id),
    }));

    const layoutSubmenu = [
      {
        label: 'Grid',
        onSelect: () => {
          const positions = gridLayout(pinned.length);
          actions.setPinned(pinned.map((p, i) => ({ ...p, ...positions[i] })));
          actions.flash('Grid layout applied');
        },
      },
      {
        label: 'Circle',
        onSelect: () => {
          const positions = circleLayout(pinned.length);
          actions.setPinned(pinned.map((p, i) => ({ ...p, ...positions[i] })));
          actions.flash('Circle layout applied');
        },
      },
      {
        label: 'Layered (by type)',
        onSelect: () => {
          actions.setPinned(layeredLayout(pinned, entityById));
          actions.flash('Layered layout applied');
        },
      },
    ];

    return [
      { heading: 'Canvas' },
      {
        label: 'Auto-layout',
        icon: '◎',
        disabled: !hasContent,
        submenu: layoutSubmenu,
      },
      {
        label: 'Load preset thread',
        icon: '⟐',
        submenu: threadSubmenu,
      },
      SEP,
      {
        label: 'Paste default link-type here',
        hint: nx.linkStyle(state.linkType).label,
        icon: '⇢',
        disabled: true,
      },
      {
        label: 'Switch mode',
        icon: '⎇',
        submenu: [
          { label: 'Select',   onSelect: () => actions.setMode('select') },
          { label: 'Connect',  onSelect: () => actions.setMode('connect') },
          { label: 'Annotate', onSelect: () => actions.setMode('annotate') },
        ],
      },
      SEP,
      {
        label: 'Save thread',
        icon: '◉',
        hint: `${pinned.length} · ${links.length}`,
        disabled: !hasContent,
        onSelect: () => actions.saveThread(),
      },
      {
        label: 'Clear canvas',
        icon: '×',
        danger: true,
        disabled: !hasContent,
        onSelect: () => actions.clearCanvas(),
      },
    ];
  }

  // ── Card menu (on an entity) ─────────────────────────────────────────────
  function cardMenu(ctx) {
    const { entityId, state, actions, data, entityById, nx } = ctx;
    const e = entityById[entityId];
    if (!e) return [];
    const { pinned, links } = state;
    const myLinks = links.filter(l => l.from === entityId || l.to === entityId);

    const neighbors = neighborsOf(entityId, links);

    // Links on canvas as submenu
    const linksSubmenu = myLinks.length ? myLinks.map(l => {
      const otherId = l.from === entityId ? l.to : l.from;
      const arrow = l.from === entityId ? '→' : '←';
      const ls = nx.linkStyle(l.type);
      return {
        label: `${arrow} ${ls.label} · ${entityById[otherId]?.name || otherId}`,
        hint: 'remove',
        danger: true,
        onSelect: () => {
          actions.setLinks(ls => ls.filter(x => x.id !== l.id));
          actions.flash('Link removed');
        },
      };
    }) : [{ label: 'No links on canvas', disabled: true }];

    // Data-driven "expand": neighbors in data.links that aren't yet pinned.
    const dataNeighbors = data.links
      .filter(l => (l.from === entityId || l.to === entityId))
      .map(l => l.from === entityId ? l.to : l.from)
      .filter(id => !pinned.some(p => p.entityId === id));
    const uniqueDataNeighbors = Array.from(new Set(dataNeighbors));

    const alignSubmenu = [
      { label: 'Bring to front', onSelect: () => bringToFront(ctx) },
      { label: 'Send to back',   onSelect: () => sendToBack(ctx) },
      SEP,
      { label: 'Align left',     onSelect: () => alignPinned(ctx, 'left') },
      { label: 'Align center',   onSelect: () => alignPinned(ctx, 'centerX') },
      { label: 'Align right',    onSelect: () => alignPinned(ctx, 'right') },
      SEP,
      { label: 'Align top',      onSelect: () => alignPinned(ctx, 'top') },
      { label: 'Align middle',   onSelect: () => alignPinned(ctx, 'centerY') },
      { label: 'Align bottom',   onSelect: () => alignPinned(ctx, 'bottom') },
    ];

    return [
      { heading: `${nx.typeStyle(e.type).label} · ${e.name}` },
      {
        label: 'Open details',
        icon: '◉',
        onSelect: () => { actions.setSelected(entityId); actions.setMode('select'); },
      },
      {
        label: 'Connect from here…',
        icon: '⇢',
        hint: nx.linkStyle(state.linkType).label,
        onSelect: () => { actions.setMode('connect'); actions.setLinkStart(entityId); actions.flash('Pick target to link'); },
      },
      {
        label: 'Change default link type',
        icon: '⎇',
        submenu: LINK_TYPES.map(t => ({
          label: nx.linkStyle(t).label,
          hint: t === state.linkType ? 'current' : undefined,
          onSelect: () => actions.setLinkType(t),
        })),
      },
      SEP,
      {
        label: 'Expand related',
        icon: '⊕',
        hint: uniqueDataNeighbors.length ? String(uniqueDataNeighbors.length) : '0',
        disabled: uniqueDataNeighbors.length === 0,
        onSelect: () => expandRelated(ctx, uniqueDataNeighbors),
      },
      {
        label: 'Focus on this',
        icon: '◎',
        hint: `+${neighbors.size} neighbors`,
        disabled: pinned.length <= 1,
        onSelect: () => focusOn(ctx, entityId, neighbors),
      },
      {
        label: 'Arrange / z-order',
        icon: '⇅',
        submenu: alignSubmenu,
      },
      SEP,
      {
        label: 'Duplicate card',
        icon: '⎘',
        disabled: true,
        hint: 'entities are unique',
      },
      {
        label: 'Copy entity ID',
        icon: '⎘',
        hint: entityId,
        onSelect: () => { copyToClipboard(entityId); actions.flash(`Copied ${entityId}`); },
      },
      {
        label: 'Links on canvas',
        icon: '⇢',
        hint: String(myLinks.length),
        submenu: linksSubmenu,
      },
      SEP,
      {
        label: 'Remove from canvas',
        icon: '×',
        danger: true,
        onSelect: () => {
          actions.setPinned(p => p.filter(pp => pp.entityId !== entityId));
          actions.setLinks(l => l.filter(ll => ll.from !== entityId && ll.to !== entityId));
          actions.flash(`Removed ${e.name}`);
        },
      },
    ];
  }

  // ── Link-label menu ──────────────────────────────────────────────────────
  function linkMenu(ctx) {
    const { linkId, state, actions, entityById, nx } = ctx;
    const link = state.links.find(l => l.id === linkId);
    if (!link) return [];
    const ls = nx.linkStyle(link.type);
    return [
      { heading: `Link · ${entityById[link.from]?.name} → ${entityById[link.to]?.name}` },
      {
        label: 'Change type',
        icon: '⎇',
        submenu: LINK_TYPES.map(t => ({
          label: nx.linkStyle(t).label,
          hint: t === link.type ? 'current' : undefined,
          onSelect: () => {
            actions.setLinks(links => links.map(l => l.id === linkId ? { ...l, type: t } : l));
            actions.flash(`Link type → ${nx.linkStyle(t).label}`);
          },
        })),
      },
      {
        label: 'Reverse direction',
        icon: '⇄',
        onSelect: () => {
          actions.setLinks(links => links.map(l => l.id === linkId ? { ...l, from: l.to, to: l.from } : l));
          actions.flash('Link reversed');
        },
      },
      SEP,
      {
        label: 'Remove link',
        icon: '×',
        danger: true,
        onSelect: () => {
          actions.setLinks(links => links.filter(l => l.id !== linkId));
          actions.flash('Link removed');
        },
      },
    ];
  }

  // ── Helper actions implementing card-menu submenu items ──────────────────
  function bringToFront(ctx) {
    const { entityId, state, actions } = ctx;
    const p = state.pinned.find(pp => pp.entityId === entityId);
    if (!p) return;
    actions.setPinned([...state.pinned.filter(pp => pp.entityId !== entityId), p]);
  }
  function sendToBack(ctx) {
    const { entityId, state, actions } = ctx;
    const p = state.pinned.find(pp => pp.entityId === entityId);
    if (!p) return;
    actions.setPinned([p, ...state.pinned.filter(pp => pp.entityId !== entityId)]);
  }

  function alignPinned(ctx, how) {
    const { entityId, state, actions } = ctx;
    const me = state.pinned.find(p => p.entityId === entityId);
    if (!me) return;
    const anchor = {
      left: me.x,
      right: me.x,
      centerX: me.x + CARD_W / 2,
      top: me.y,
      bottom: me.y,
      centerY: me.y + CARD_H / 2,
    }[how];
    actions.setPinned(state.pinned.map(p => {
      if (p.entityId === entityId) return p;
      switch (how) {
        case 'left':    return { ...p, x: anchor };
        case 'right':   return { ...p, x: anchor + CARD_W - CARD_W };
        case 'centerX': return { ...p, x: anchor - CARD_W / 2 };
        case 'top':     return { ...p, y: anchor };
        case 'bottom':  return { ...p, y: anchor + CARD_H - CARD_H };
        case 'centerY': return { ...p, y: anchor - CARD_H / 2 };
        default:        return p;
      }
    }));
    actions.flash(`Aligned ${how}`);
  }

  function expandRelated(ctx, neighborIds) {
    const { state, actions, data } = ctx;
    const existing = new Set(state.pinned.map(p => p.entityId));
    const toAdd = neighborIds.filter(id => !existing.has(id)).slice(0, 8);
    if (!toAdd.length) { actions.flash('No new neighbors'); return; }
    const n0 = state.pinned.length;
    const cols = 4;
    const added = toAdd.map((id, i) => ({
      entityId: id,
      x: 60 + ((n0 + i) % cols) * 200,
      y: 50 + Math.floor((n0 + i) / cols) * 120,
    }));
    const nextPinned = [...state.pinned, ...added];
    const canvasIds = new Set(nextPinned.map(p => p.entityId));
    const newLinks = data.links
      .filter(l => canvasIds.has(l.from) && canvasIds.has(l.to))
      .map(l => ({ id: l.id, from: l.from, to: l.to, type: l.type }));
    // Merge while preserving any user-created links not in data.links
    const userMade = state.links.filter(l => !data.links.some(d => d.id === l.id));
    actions.setPinned(nextPinned);
    actions.setLinks([...newLinks, ...userMade]);
    actions.flash(`Expanded · +${added.length} related`);
  }

  function focusOn(ctx, entityId, neighbors) {
    const { state, actions } = ctx;
    const keep = new Set([entityId, ...neighbors]);
    const filtered = state.pinned.filter(p => keep.has(p.entityId));
    // Place focal entity in center; neighbors on a circle around it.
    const center = { x: 420, y: 240 };
    const others = filtered.filter(p => p.entityId !== entityId);
    const positions = circleLayout(others.length, { x: center.x + CARD_W / 2, y: center.y + CARD_H / 2 }, 200);
    actions.setPinned([
      { entityId, x: center.x, y: center.y },
      ...others.map((p, i) => ({ entityId: p.entityId, ...positions[i] })),
    ]);
    actions.setLinks(state.links.filter(l => keep.has(l.from) && keep.has(l.to)));
    actions.setSelected(entityId);
    actions.flash('Focused');
  }

  // ── Export factories under a stable namespace ───────────────────────────
  window.NexusCanvasActions = {
    backgroundMenu,
    cardMenu,
    linkMenu,
  };
})();
