// groups.jsx — the board: an "Ungrouped" inbox plus titled grouping cards.
//
// The board model is a flat object:
//   { ungrouped: string[], cards: [{ id, title, tickers: string[] }] }
// New tickers land in `ungrouped`; the analyst drags rows into cards (and
// between cards, and back). Every container — the inbox and each card — renders
// the same LedgerTable, so the columns line up across the whole board.
//
// Row drag is cross-container and pointer-driven (App owns the state). While a
// drag is live the lifted ticker is removed from its list and a placeholder gap
// is shown wherever it would drop; a position:fixed clone follows the pointer.

// ── Board model ──────────────────────────────────────────────────────────
const DEFAULT_TICKERS = [
  "AAPL", "MSFT", "NVDA", "TSLA", "SAP.DE", "ASML.AS", "NOVO-B.CO", "9988.HK",
];

function loadBoard() {
  try {
    const raw = localStorage.getItem("lsm.board.v1");
    if (raw) {
      const b = JSON.parse(raw);
      if (b && Array.isArray(b.ungrouped) && Array.isArray(b.cards)) return b;
    }
  } catch (e) {}
  // Migrate the legacy flat watchlist into the inbox.
  try {
    const legacy = localStorage.getItem("lsm.tickers");
    if (legacy) {
      const arr = JSON.parse(legacy);
      if (Array.isArray(arr)) return { ungrouped: arr, cards: [] };
    }
  } catch (e) {}
  return { ungrouped: [...DEFAULT_TICKERS], cards: [] };
}

function saveBoard(board) {
  try {
    localStorage.setItem("lsm.board.v1", JSON.stringify(board));
  } catch (e) {}
}

// Every ticker on the board, in display order (inbox first, then each card).
function boardAllTickers(board) {
  return [board.ungrouped, ...board.cards.map((c) => c.tickers)].reduce(
    (acc, list) => acc.concat(list), []
  );
}

function boardHasTicker(board, ticker) {
  return boardAllTickers(board).includes(ticker);
}

// The ticker list for one container id ("ungrouped" or a card id).
function containerTickers(board, containerId) {
  if (containerId === "ungrouped") return board.ungrouped;
  const card = board.cards.find((c) => c.id === containerId);
  return card ? card.tickers : [];
}

// ── Board transforms — each returns a new board ──────────────────────────
function boardAddTicker(board, ticker) {
  return { ...board, ungrouped: [...board.ungrouped, ticker] };
}

function boardRemoveTicker(board, ticker) {
  return {
    ungrouped: board.ungrouped.filter((t) => t !== ticker),
    cards: board.cards.map((c) => ({
      ...c,
      tickers: c.tickers.filter((t) => t !== ticker),
    })),
  };
}

function clampIndex(i, len) {
  if (i == null || i < 0) return len;
  return Math.min(i, len);
}

// Move `ticker` to `toContainer` at `toIndex`, removing it from wherever it is.
function boardMoveTicker(board, ticker, toContainer, toIndex) {
  const stripped = boardRemoveTicker(board, ticker);
  if (toContainer === "ungrouped") {
    const next = [...stripped.ungrouped];
    next.splice(clampIndex(toIndex, next.length), 0, ticker);
    return { ...stripped, ungrouped: next };
  }
  return {
    ...stripped,
    cards: stripped.cards.map((c) => {
      if (c.id !== toContainer) return c;
      const next = [...c.tickers];
      next.splice(clampIndex(toIndex, next.length), 0, ticker);
      return { ...c, tickers: next };
    }),
  };
}

function boardCreateCard(board) {
  const card = { id: "g" + Date.now().toString(36), title: "", tickers: [] };
  return { ...board, cards: [...board.cards, card] };
}

function boardRenameCard(board, id, title) {
  return {
    ...board,
    cards: board.cards.map((c) => (c.id === id ? { ...c, title } : c)),
  };
}

// Delete a card — its tickers fall back into the inbox so nothing is lost.
function boardDeleteCard(board, id) {
  const card = board.cards.find((c) => c.id === id);
  if (!card) return board;
  return {
    ungrouped: [...board.ungrouped, ...card.tickers],
    cards: board.cards.filter((c) => c.id !== id),
  };
}

// Move a card to a new index in the card list (drag-reorder).
function boardMoveCard(board, cardId, toIndex) {
  const card = board.cards.find((c) => c.id === cardId);
  if (!card) return board;
  const next = board.cards.filter((c) => c.id !== cardId);
  next.splice(clampIndex(toIndex, next.length), 0, card);
  return { ...board, cards: next };
}

// Per-card transform during a card drag — the lifted card tracks the pointer,
// the cards it has crossed slide one slot to open a gap ("the rail"). `slot` is
// the dragged card's height plus the inter-card gap.
function cardDragVisual(cardId, index, cardDrag) {
  if (!cardDrag || !cardDrag.armed) return { style: undefined, extraClass: "" };
  const { fromIndex, overIndex, offsetY, slot, moved } = cardDrag;
  if (cardId === cardDrag.cardId) {
    return {
      style: {
        transform: "translateY(" + offsetY + "px)",
        transition: "none",
        position: "relative",
        zIndex: 40,
      },
      extraClass: " group-card--lifted",
    };
  }
  let shift = 0;
  if (moved) {
    if (overIndex > fromIndex && index > fromIndex && index <= overIndex) {
      shift = -slot;
    } else if (overIndex < fromIndex && index >= overIndex && index < fromIndex) {
      shift = slot;
    }
  }
  return {
    style: {
      transform: shift ? "translateY(" + shift + "px)" : "none",
      transition: "transform .18s cubic-bezier(.2,.8,.2,1)",
    },
    extraClass: "",
  };
}

// ── LedgerHead — the draggable column-header row, shared by every table ──
function LedgerHead({ columnOrder, dragId, dropId, setDragId, setDropId, moveColumn }) {
  return (
    <thead>
      <tr>
        <th className="ld__h-sym">Symbol</th>
        {columnOrder.map((id) => {
          const col = COLUMN_BY_ID[id];
          if (!col) return null;
          const cls = "ld__h-num ld__h-drag ld__h-" + id
            + (dragId === id ? " ld__h--dragging" : "")
            + (dropId === id && dragId && dragId !== id ? " ld__h--drop" : "");
          return (
            <th key={id} className={cls} scope="col"
                draggable
                title="Drag to reorder"
                onDragStart={() => setDragId(id)}
                onDragOver={(e) => {
                  e.preventDefault();
                  if (dropId !== id) setDropId(id);
                }}
                onDragLeave={() => setDropId((d) => (d === id ? null : d))}
                onDrop={(e) => {
                  e.preventDefault();
                  if (dragId) moveColumn(dragId, id);
                  setDragId(null);
                  setDropId(null);
                }}
                onDragEnd={() => { setDragId(null); setDropId(null); }}>
              {col.label}
            </th>
          );
        })}
      </tr>
    </thead>
  );
}

// ── LedgerTable — one container's table (the inbox or a card) ────────────
function LedgerTable({
  containerId, tickers, emptyHint, columnOrder, rowDrag,
  onRowPointerDown, onOpenTicker,
  dragId, dropId, setDragId, setDropId, moveColumn,
}) {
  const span = columnOrder.length + 1;
  // Mid-drag the lifted ticker is pulled out of its list; a placeholder shows
  // where it would land in whichever container the pointer is over.
  const dragging = rowDrag && rowDrag.active;
  const rows = dragging
    ? tickers.filter((t) => t !== rowDrag.ticker)
    : tickers;
  const placeholderAt =
    dragging && rowDrag.overContainer === containerId
      ? Math.min(rowDrag.overIndex, rows.length)
      : -1;

  return (
    <table className="ledger">
      <LedgerHead
        columnOrder={columnOrder}
        dragId={dragId} dropId={dropId}
        setDragId={setDragId} setDropId={setDropId} moveColumn={moveColumn}
      />
      <tbody data-container-id={containerId}>
        {rows.length === 0 && placeholderAt < 0 && (
          <tr className="ld__empty">
            <td colSpan={span}>{emptyHint}</td>
          </tr>
        )}
        {rows.map((ticker, i) => (
          <React.Fragment key={ticker}>
            {i === placeholderAt && (
              <tr className="ld__row--placeholder"
                  style={{ height: rowDrag.rowHeight }}>
                <td colSpan={span} />
              </tr>
            )}
            <StockRow
              ticker={ticker}
              columnOrder={columnOrder}
              onPointerDown={(e, tk) => onRowPointerDown(e, tk, containerId)}
              onOpen={() => onOpenTicker(ticker)}
            />
          </React.Fragment>
        ))}
        {placeholderAt >= rows.length && (
          <tr className="ld__row--placeholder"
              style={{ height: rowDrag.rowHeight }}>
            <td colSpan={span} />
          </tr>
        )}
      </tbody>
    </table>
  );
}

// ── GroupCard — a titled container wrapping a LedgerTable ────────────────
function GroupCard({
  card, index, cardDrag, onCardPointerDown,
  columnOrder, rowDrag, onRowPointerDown, onOpenTicker,
  onRename, onDelete, dragId, dropId, setDragId, setDropId, moveColumn,
}) {
  const [confirming, setConfirming] = React.useState(false);
  const isOver = rowDrag && rowDrag.active && rowDrag.overContainer === card.id;
  const v = cardDragVisual(card.id, index, cardDrag);
  return (
    <section
      className={"group-card" + (isOver ? " group-card--over" : "") + v.extraClass}
      style={v.style}>
      <header
        className="group-card__head"
        onPointerDown={(e) => onCardPointerDown(e, card.id)}>
        <input
          className="group-card__title"
          value={card.title}
          placeholder="Untitled group"
          spellCheck="false"
          onChange={(e) => onRename(card.id, e.target.value)}
        />
        <span className="group-card__count">
          {card.tickers.length}
          {card.tickers.length === 1 ? " company" : " companies"}
        </span>
        {!confirming ? (
          <button
            className="group-card__del"
            title="Delete group"
            aria-label="Delete group"
            onPointerDown={(e) => e.stopPropagation()}
            onClick={() => setConfirming(true)}>
            <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
              <path d="M2 4h12 M6 4V2.5h4V4 M3.6 4l.7 9a1 1 0 0 0 1 .95h5.4a1 1 0 0 0 1-.95l.7-9 M6.6 6.4v5.2 M9.4 6.4v5.2"
                    stroke="currentColor" strokeWidth="1.1" fill="none"
                    strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </button>
        ) : (
          <span className="group-card__confirm"
                onPointerDown={(e) => e.stopPropagation()}>
            <span className="group-card__confirm-msg">Delete this group?</span>
            <button className="group-card__confirm-keep"
                    onClick={() => setConfirming(false)}>Keep</button>
            <button className="group-card__confirm-go"
                    onClick={() => onDelete(card.id)}>Delete</button>
          </span>
        )}
      </header>
      <LedgerTable
        containerId={card.id}
        tickers={card.tickers}
        emptyHint="Drag companies into this group"
        columnOrder={columnOrder}
        rowDrag={rowDrag}
        onRowPointerDown={onRowPointerDown}
        onOpenTicker={onOpenTicker}
        dragId={dragId} dropId={dropId}
        setDragId={setDragId} setDropId={setDropId} moveColumn={moveColumn}
      />
    </section>
  );
}

// ── RowDragClone — the position:fixed row that follows the pointer ───────
// A one-row table; <colgroup> carries the same .ld__h-* width classes as the
// real headers, so table-layout:fixed lays the clone out identically.
function RowDragClone({ rowDrag, columnOrder }) {
  const base = { width: rowDrag.rowWidth };
  const style = rowDrag.settling
    ? {
        ...base,
        left: rowDrag.settleX,
        top: rowDrag.settleY,
        transition: "left .16s ease, top .16s ease",
      }
    : {
        ...base,
        left: rowDrag.pointerX - rowDrag.grabDX,
        top: rowDrag.pointerY - rowDrag.grabDY,
      };
  return (
    <div className="row-clone" style={style}>
      <table className="ledger">
        <colgroup>
          <col className="ld__h-sym" />
          {columnOrder.map((id) => <col key={id} className={"ld__h-" + id} />)}
        </colgroup>
        <tbody>
          <StockRow
            ticker={rowDrag.ticker}
            columnOrder={columnOrder}
            extraClass=" ld__row--lifted"
            onPointerDown={() => {}}
            onOpen={() => {}}
          />
        </tbody>
      </table>
    </div>
  );
}

// Hit-test: which container + insertion index is the pointer over? Returns
// null when the pointer isn't over any container. Pure DOM read — every
// LedgerTable's <tbody> carries data-container-id, so no ref plumbing.
function hitTestContainers(x, y) {
  const tbodies = document.querySelectorAll("tbody[data-container-id]");
  for (let t = 0; t < tbodies.length; t++) {
    const tbody = tbodies[t];
    const r = tbody.getBoundingClientRect();
    if (y < r.top - 28 || y > r.bottom + 28) continue;
    if (x < r.left - 48 || x > r.right + 48) continue;
    const rows = tbody.querySelectorAll("tr.ld__row:not(.ld__row--placeholder)");
    let index = rows.length;
    for (let i = 0; i < rows.length; i++) {
      const rr = rows[i].getBoundingClientRect();
      if (y < rr.top + rr.height / 2) { index = i; break; }
    }
    return { containerId: tbody.dataset.containerId, index };
  }
  return null;
}

Object.assign(window, {
  DEFAULT_TICKERS,
  loadBoard, saveBoard,
  boardAllTickers, boardHasTicker, containerTickers,
  boardAddTicker, boardRemoveTicker, boardMoveTicker,
  boardCreateCard, boardRenameCard, boardDeleteCard, boardMoveCard,
  cardDragVisual,
  LedgerTable, GroupCard, RowDragClone, hitTestContainers,
});
