/**
 * `AnnouncementsProvider` — the live, app-wide store for inter-agent announcements
 * (ticket #4, F1 wiring). Subscribes **once** to the domain-event stream via the
 * {@link SystemGateway} and folds two orthogonal signals:
 *
 *   1. **Announcement content** — `agentAnnouncement` events appended to the
 *      bounded `(target, ticketId)` index (the scrolling text of the overlay F3
 *      and the requester preview F2).
 *   2. **Overlay lifecycle** — the per-agent **busy/idle** state, the single
 *      authority for mounting/retracting the target overlay (Architect arbitrage
 *      2026-07-04). Fed live by `agentBusyChanged { agentId, busy }` and hydrated
 *      from the reconciled read-model `ProjectWorkState.agents[].busy` at mount /
 *      reboot (via {@link useHydrateAgentBusy}).
 *
 * Why busy — not "there are announcements" — drives F3: a turn can end **without**
 * any completion event (interruption / error / crash / rate-limit). The mediator's
 * `agentBusyChanged` `busy:false` falls at idle in *every* case, and the read-model
 * is reconciled on reboot, so the overlay can never stick. A rate-limited agent
 * stays `busy:true`, so its overlay persists with no special-casing.
 *
 * (On this `develop` base there is no canonical `agentTurnEvent`/`final` signal;
 * busy is the sole lifecycle authority, which is exactly what we want.)
 *
 * Consumers read via {@link useTargetAnnouncements} (F3) and
 * {@link useRequesterAnnouncements} (F2). The store is never persisted — it is
 * rebuilt live from the event stream and the read-model snapshot.
 */

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from "react";

import { useGateways } from "@/app/di";
import {
  announcementsForRequester,
  announcementsForTarget,
  appendAnnouncement,
  emptyAnnouncementIndex,
  purgeAllForTarget,
  type Announcement,
  type AnnouncementIndex,
} from "./announcementsStore";

/** Per-agent busy map: `true` while the agent owns a turn (drives the overlay). */
type BusyMap = Record<string, boolean>;

interface AnnouncementsStore {
  /** Bounded announcement content, indexed by `(target, ticketId)`. */
  index: AnnouncementIndex;
  /** Per-agent busy state — the overlay's lifecycle authority. */
  busy: BusyMap;
  /**
   * Seeds an agent's busy state from the read-model, but only when no live signal
   * is yet known for it: live `agentBusyChanged` events always win over a
   * (possibly staler) hydration snapshot.
   */
  seedBusy: (agentId: string, busy: boolean) => void;
}

const noopSeed = () => {};
const EMPTY_INDEX = emptyAnnouncementIndex();
const EMPTY_BUSY: BusyMap = {};

/** Store shape read outside a provider — a silent, inert default (never throws). */
const DEFAULT_STORE: AnnouncementsStore = {
  index: EMPTY_INDEX,
  busy: EMPTY_BUSY,
  seedBusy: noopSeed,
};

const AnnouncementsContext = createContext<AnnouncementsStore | null>(null);

export function AnnouncementsProvider({ children }: { children: ReactNode }) {
  const { system } = useGateways();
  const [index, setIndex] = useState<AnnouncementIndex>(emptyAnnouncementIndex);
  const [busy, setBusy] = useState<BusyMap>({});

  // Hydration seed: apply only when the agent is unknown, so a live event that
  // already landed (or lands before the async read-model resolves) is not clobbered.
  const seedBusy = useCallback((agentId: string, value: boolean) => {
    setBusy((prev) => (agentId in prev ? prev : { ...prev, [agentId]: value }));
  }, []);

  useEffect(() => {
    // `system` may be absent in unit tests injecting a partial gateway set.
    if (!system) return;
    let unsubscribe: (() => void) | undefined;
    let cancelled = false;

    void system
      .onDomainEvent((event) => {
        if (event.type === "agentAnnouncement") {
          // Content of the overlay/preview.
          setIndex((prev) =>
            appendAnnouncement(prev, {
              requester: event.requester,
              target: event.target,
              ticketId: event.ticketId,
              text: event.text,
              atMs: event.atMs,
            }),
          );
        } else if (event.type === "agentBusyChanged") {
          // Lifecycle authority. `true` mounts the target overlay; `false` retracts
          // it (idle) and clears the target's content so a next turn starts fresh —
          // even when no completion event was ever emitted (the sticking case).
          const { agentId, busy: isBusy } = event;
          setBusy((prev) => ({ ...prev, [agentId]: isBusy }));
          if (!isBusy) setIndex((prev) => purgeAllForTarget(prev, agentId));
        }
      })
      .then((un) => {
        if (cancelled) un();
        else unsubscribe = un;
      })
      .catch(() => {
        // Event relay unavailable in this environment — the store stays empty.
      });

    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [system]);

  const store = useMemo<AnnouncementsStore>(
    () => ({ index, busy, seedBusy }),
    [index, busy, seedBusy],
  );

  return (
    <AnnouncementsContext.Provider value={store}>
      {children}
    </AnnouncementsContext.Provider>
  );
}

/** The store, or an inert default when read outside a provider. */
function useStore(): AnnouncementsStore {
  return useContext(AnnouncementsContext) ?? DEFAULT_STORE;
}

/** Whether a real provider is mounted above (hydration only runs when it is). */
function useWithinProvider(): boolean {
  return useContext(AnnouncementsContext) !== null;
}

/**
 * Announcements destined to `target` (F3): the flattened, time-ordered content
 * plus `active` — whether the overlay should be mounted. `active` is the target's
 * **busy** state (the lifecycle authority), NOT the mere presence of announcements.
 */
export function useTargetAnnouncements(target: string): {
  announcements: Announcement[];
  active: boolean;
} {
  const { index, busy } = useStore();
  return useMemo(
    () => ({
      announcements: announcementsForTarget(index, target),
      active: busy[target] === true,
    }),
    [index, busy, target],
  );
}

/**
 * Hydrates the target's busy state from the reconciled read-model at mount/reboot,
 * so an overlay mounts for a turn already in flight when its cell appears — no live
 * `agentBusyChanged` will replay for it. Live events subsequently win (see
 * `seedBusy`). Guarded: no-op outside a provider or without a `workState` gateway.
 */
export function useHydrateAgentBusy(projectId: string, agentId: string): void {
  const { workState } = useGateways();
  const { seedBusy } = useStore();
  const within = useWithinProvider();

  useEffect(() => {
    if (!within || !workState) return;
    let cancelled = false;
    void workState
      .getProjectWorkState(projectId)
      .then((state) => {
        if (cancelled) return;
        const agent = state.agents.find((a) => a.agentId === agentId);
        if (agent) seedBusy(agentId, agent.busy.state === "busy");
      })
      .catch(() => {
        // Read-model unavailable — fall back to the live event stream only.
      });
    return () => {
      cancelled = true;
    };
  }, [within, workState, projectId, agentId, seedBusy]);
}

/**
 * Announcements the given `requester` is waiting on (F2), filtered by
 * `requester == self` so a shared target thread never leaks another requester's
 * announcements. Optionally scoped to a single `ticketId`.
 */
export function useRequesterAnnouncements(
  requester: string,
  ticketId?: string,
): Announcement[] {
  const { index } = useStore();
  return useMemo(
    () => announcementsForRequester(index, requester, ticketId),
    [index, requester, ticketId],
  );
}
