// Live event pane — Preact owner of the DOM node `useLiveStream`'s rows
// get rendered into. Deliberately NOT the old MutationObserver + rAF
// snap-animation + `smoothScrollingUntil` gate machinery
// (@hive/shared/terminal.js) — mara explicitly flagged that the old
// page's autoscroll breaks when new events land while the operator has
// scrolled up, and not to copy it 1:1. This is the "do it properly the
// preact way" version:
//
//   - `stickToBottom` is plain state, updated synchronously from the
//     scroll handler (near-bottom → true, scrolled away → false). No
//     separate "am I mid-animation" gate to fight with it — there's no
//     animation to fight, snapping is an instant `scrollTop` write in a
//     `useLayoutEffect` that runs once after Preact has already
//     committed the new rows to the DOM, so it always sees the real
//     `scrollHeight`, never a stale pre-mutation one.
//   - A "load older" prepend is told apart from a normal append via
//     `prependingRef` (set immediately before calling `loadMore()`) so
//     the effect can choose the right response: viewport-preserving
//     scrollTop compensation for a prepend, stick-to-bottom-or-count-
//     unseen for an append. The old code inferred this from DOM
//     mutation shape after the fact; here the caller already knows
//     which one it did, so there's nothing to infer.
import { useLayoutEffect, useRef, useState, useImperativeHandle } from 'preact/hooks';
import { forwardRef } from 'preact/compat';
import { useLiveStream } from '../hooks/useLiveStream.js';
import { Row } from './Row.js';

const NEAR_BOTTOM_PX = 48;
const LOAD_MORE_SCROLL_PX = 80;

export interface LiveStreamProps {
  /** Forwarded to useLiveStream — fires on live turn_start/turn_end so
   * the caller can refresh `/api/state` sooner than its poll interval. */
  onLiveTurnBoundary?: () => void;
}

/** Imperative escape hatch for TermInput's local-only slash commands
 * (`/help`, `/clear`) — same shape as app.js's old `termAPI` object,
 * kept as a ref handle rather than lifting the whole row array up to
 * Root so LiveStream/useLiveStream still own their state privately. */
export interface LiveStreamHandle {
  pushNote: (text: string) => void;
  clear: () => void;
}

export const LiveStream = forwardRef<LiveStreamHandle, LiveStreamProps>(function LiveStream(
  { onLiveTurnBoundary },
  ref,
) {
  const { rows, hasMore, loadingMore, loadMore, pushLocalNote, clearLocal } = useLiveStream({ onLiveTurnBoundary });
  useImperativeHandle(ref, () => ({ pushNote: pushLocalNote, clear: clearLocal }), [pushLocalNote, clearLocal]);
  const logRef = useRef<HTMLDivElement>(null);
  const [stickToBottom, setStickToBottom] = useState(true);
  const [unseen, setUnseen] = useState(0);
  const prevRowCount = useRef(0);
  const prependingRef = useRef(false);
  const preScrollHeightRef = useRef(0);

  function isNearBottom(el: HTMLDivElement): boolean {
    return el.scrollHeight - el.scrollTop - el.clientHeight <= NEAR_BOTTOM_PX;
  }

  function handleScroll() {
    const el = logRef.current;
    if (!el) return;
    const nearBottom = isNearBottom(el);
    setStickToBottom(nearBottom);
    if (nearBottom) setUnseen(0);
    if (el.scrollTop <= LOAD_MORE_SCROLL_PX && hasMore && !loadingMore) startLoadMore();
  }

  function startLoadMore() {
    const el = logRef.current;
    if (!el || loadingMore || !hasMore) return;
    prependingRef.current = true;
    preScrollHeightRef.current = el.scrollHeight;
    loadMore();
  }

  function jumpToBottom() {
    const el = logRef.current;
    if (el) el.scrollTop = el.scrollHeight - el.clientHeight;
    setStickToBottom(true);
    setUnseen(0);
  }

  // Runs after every commit that changed `rows` — i.e. after the browser
  // has already laid out the new/coalesced content, so `scrollHeight`
  // below is always current.
  useLayoutEffect(() => {
    const el = logRef.current;
    if (!el) return;
    const added = rows.length - prevRowCount.current;
    prevRowCount.current = rows.length;

    if (prependingRef.current) {
      prependingRef.current = false;
      el.scrollTop += el.scrollHeight - preScrollHeightRef.current;
      return;
    }
    if (stickToBottom) {
      el.scrollTop = el.scrollHeight - el.clientHeight;
    } else if (added > 0) {
      setUnseen((n) => n + added);
    }
  }, [rows, stickToBottom]);

  return (
    <div className="terminal-wrap">
      <div className="live terminal" ref={logRef} onScroll={handleScroll}>
        {hasMore && (
          <button type="button" className="load-more-pill" disabled={loadingMore} onClick={startLoadMore}>
            {loadingMore ? '↑ loading…' : '↑ load older'}
          </button>
        )}
        {rows.map((row) => (
          <Row key={row.key} row={row} />
        ))}
      </div>
      {unseen > 0 && (
        <button type="button" className="tail-pill visible" onClick={jumpToBottom}>
          ↓ {unseen} new
        </button>
      )}
    </div>
  );
});
