/**
 * View-model hook for the human conversation transcript viewer (LS7).
 *
 * Read-only and cold (a human action, never on the agent hot path): it consumes
 * the {@link ConversationGateway} port only. The buffer is kept in chronological
 * ascending order and deduplicated by turn id:
 *
 * - **open** → `readPage({ direction: "backward" })` yields the latest page
 *   (bottom of the thread);
 * - **loadOlder** → `readPage` backward anchored on the *oldest* buffered turn,
 *   **prepended** without duplicates (scroll-up);
 * - **refreshTail** → `readPage` forward anchored on the *newest* buffered turn,
 *   appended; loops until the tail is exhausted. Triggered manually and on the
 *   same domain events the Work panel already listens to.
 */

import { useCallback, useEffect, useRef, useState } from "react";

import type { GatewayError, TurnView } from "@/domain";
import { useGateways } from "@/app/di";

export interface ConversationThreadViewModel {
  /** Buffered turns, oldest → newest. */
  turns: TurnView[];
  /** A page request is in flight. */
  busy: boolean;
  /** Last error message, or `null`. */
  error: string | null;
  /** Whether older turns exist before the buffer (scroll-up sentinel). */
  hasMore: boolean;
  /** Loads the page of turns just before the oldest buffered one (prepend). */
  loadOlder: () => Promise<void>;
  /** Catches the buffer up to the newest turns of the thread (append). */
  refreshTail: () => Promise<void>;
}

function describe(e: unknown): string {
  if (e && typeof e === "object" && "message" in e) {
    return String((e as GatewayError).message);
  }
  return String(e);
}

/** Dedup by id, preserving order (incoming pages never reorder the thread). */
function dedupById(turns: TurnView[]): TurnView[] {
  const seen = new Set<string>();
  const out: TurnView[] = [];
  for (const turn of turns) {
    if (seen.has(turn.id)) continue;
    seen.add(turn.id);
    out.push(turn);
  }
  return out;
}

export function useConversationThread(
  projectId: string,
  conversationId: string,
): ConversationThreadViewModel {
  const { conversation } = useGateways();
  const [turns, setTurns] = useState<TurnView[]>([]);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(false);
  // Mirror the buffer in a ref so callbacks can anchor without re-creating on
  // every turn change (and without racing concurrent loads).
  const turnsRef = useRef<TurnView[]>([]);
  const inFlight = useRef(false);

  const setBuffer = useCallback((next: TurnView[]) => {
    turnsRef.current = next;
    setTurns(next);
  }, []);

  const open = useCallback(async () => {
    if (inFlight.current) return;
    inFlight.current = true;
    setBusy(true);
    setError(null);
    try {
      const page = await conversation.readPage(projectId, conversationId, {
        direction: "backward",
      });
      setBuffer(dedupById(page.turns));
      setHasMore(page.hasMore);
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
      inFlight.current = false;
    }
  }, [conversation, projectId, conversationId, setBuffer]);

  const loadOlder = useCallback(async () => {
    if (inFlight.current) return;
    const oldest = turnsRef.current[0];
    if (!oldest) {
      await open();
      return;
    }
    inFlight.current = true;
    setBusy(true);
    setError(null);
    try {
      const page = await conversation.readPage(projectId, conversationId, {
        direction: "backward",
        anchor: oldest.id,
      });
      if (page.turns.length > 0) {
        setBuffer(dedupById([...page.turns, ...turnsRef.current]));
      }
      setHasMore(page.hasMore);
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
      inFlight.current = false;
    }
  }, [conversation, projectId, conversationId, open, setBuffer]);

  const refreshTail = useCallback(async () => {
    if (inFlight.current) return;
    if (turnsRef.current.length === 0) {
      await open();
      return;
    }
    inFlight.current = true;
    setBusy(true);
    setError(null);
    try {
      // Walk forward until the tail is exhausted (bounded by the thread length).
      // `hasMore` (older side) is intentionally left untouched by a tail refresh.
      for (;;) {
        const newest = turnsRef.current.at(-1);
        if (!newest) break;
        const page = await conversation.readPage(projectId, conversationId, {
          direction: "forward",
          anchor: newest.id,
        });
        if (page.turns.length === 0) break;
        setBuffer(dedupById([...turnsRef.current, ...page.turns]));
        if (!page.hasMore) break;
      }
    } catch (e) {
      setError(describe(e));
    } finally {
      setBusy(false);
      inFlight.current = false;
    }
  }, [conversation, projectId, conversationId, open, setBuffer]);

  // (Re)open whenever the target conversation changes.
  useEffect(() => {
    setBuffer([]);
    setHasMore(false);
    void open();
  }, [open, setBuffer]);

  return { turns, busy, error, hasMore, loadOlder, refreshTail };
}
