"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ExecutionPhase, FileChangeSetStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
import { isTerminalPhase } from "../execution/execution-phases.js";
import { useStigmer } from "../hooks.js";
import { toError } from "../internal/toError.js";
import { useConversationStoreRef } from "../internal/store/index.js";
import { useCreateAgentExecution } from "../execution/useCreateAgentExecution.js";
import { useExecutionStream } from "../execution/useExecutionStream.js";
import { useAgentExecutionActions } from "../execution/useAgentExecutionActions.js";
import { useSubmitApproval } from "../execution/useSubmitApproval.js";
import { useFileReview } from "../execution/useFileReview.js";
import { useSession } from "./useSession.js";
import { useSessionExecutions } from "./useSessionExecutions.js";
import { useUpdateSession } from "./useUpdateSession.js";
import { useLocalSessionWorker } from "./useLocalSessionWorker.js";
import { specWorkspaceToInput, specMcpUsagesToInput, specSkillRefsToInput, } from "./session-spec-converters.js";
/**
 * Cadence for re-discovering the session's executions while the live stream
 * cannot be relied on (a created-but-not-yet-listed execution, a silent
 * connect-timeout, or an exhausted stream error). Disabled the instant the
 * stream is healthy or terminal, so this never competes with the live feed.
 */
const REDISCOVERY_POLL_INTERVAL_MS = 5_000;
/**
 * Behavior hook that encapsulates the full conversation lifecycle for
 * a session: loading data, streaming the active execution, submitting
 * follow-up messages, and updating session-level workspace entries.
 *
 * Composes {@link useSession}, {@link useSessionExecutions},
 * {@link useCreateAgentExecution}, {@link useExecutionStream}, and
 * {@link useUpdateSession} into a single return value that drives
 * both {@link MessageThread} and {@link SessionComposer}.
 *
 * Platform builders get the complete conversation loop without
 * reimplementing orchestration logic.
 *
 * For local execution, this hook also owns the session's runner worker
 * lifecycle: while the session is open it keeps a worker polling the
 * session task queue, and it tears the worker down on close. This is a
 * no-op unless a `runnerAdapter` is configured and the session runs
 * locally, so it is safe in every environment.
 *
 * @param sessionId - Session to display and converse in. Pass `null` to skip.
 * @param org - Organization slug for creating follow-up executions.
 *
 * @example
 * ```tsx
 * function Chat({ sessionId, org }: { sessionId: string; org: string }) {
 *   const conv = useSessionConversation(sessionId, org);
 *
 *   if (conv.isLoading) return <Spinner />;
 *
 *   return (
 *     <>
 *       <MessageThread
 *         executions={conv.completedExecutions}
 *         activeStreamExecution={conv.activeStreamExecution}
 *         pendingUserMessage={conv.pendingUserMessage}
 *         onApprovalSubmit={conv.submitApproval}
 *         submittingApprovalIds={conv.submittingApprovalIds}
 *       />
 *       <SessionComposer
 *         onSubmit={(msg, model) => conv.sendFollowUp(msg, { modelName: model })}
 *         disabled={!conv.canSendFollowUp}
 *         isSubmitting={conv.isSending}
 *       />
 *     </>
 *   );
 * }
 * ```
 */
export function useSessionConversation(sessionId, org) {
    const stigmer = useStigmer();
    const { session, isLoading: sessionLoading, error: sessionError, refetch: refetchSession, } = useSession(sessionId);
    // Bounded re-discovery (see REDISCOVERY_POLL_INTERVAL_MS). The gate depends on
    // the stream below, so the decision is synced into state via an effect and fed
    // back here on the next render — a one-frame lag that is immaterial at 5s.
    const [rediscoveryActive, setRediscoveryActive] = useState(false);
    const { executions, isLoading: executionsLoading, error: executionsError, refetch, } = useSessionExecutions(sessionId, {
        refetchInterval: rediscoveryActive ? REDISCOVERY_POLL_INTERVAL_MS : false,
        // Re-list on app-relaunch / tab refocus so an execution that appeared while
        // backgrounded is picked up without the user having to act.
        refetchOnWindowFocus: true,
    });
    const { create, isCreating, clearError: clearCreateError, } = useCreateAgentExecution();
    const { update: updateSession } = useUpdateSession();
    const { submitApproval: rawSubmitApproval, submittingToolCallIds, errorsByToolCallId: approvalErrors, error: approvalError, clearError: clearApprovalError, } = useSubmitApproval();
    const { submitFileDecision: rawSubmitFileDecision, submittingDecisionKeys, decisionErrors: fileDecisionErrors, clearDecisionError: clearFileDecisionError, error: fileReviewError, clearError: clearFileReviewError, } = useFileReview();
    // Local execution: attach the session's runner worker while it is open and
    // detach it on close. No-op for cloud sessions or when no adapter is set.
    useLocalSessionWorker(sessionId, session);
    const [pendingExecutionId, setPendingExecutionId] = useState(null);
    const [pendingUserMessage, setPendingUserMessage] = useState(null);
    // Dedicated send-failure state, distinct from the create hook's internal
    // error so it can also cover the session.update() path. The last send's
    // arguments are captured for an exact retry.
    const [sendError, setSendError] = useState(null);
    const lastSendRef = useRef(null);
    const listActiveId = useMemo(() => {
        for (let i = executions.length - 1; i >= 0; i--) {
            const phase = executions[i].status?.phase;
            if (phase === undefined || !isTerminalPhase(phase)) {
                return executions[i].metadata?.id ?? null;
            }
        }
        return null;
    }, [executions]);
    const activeExecutionId = pendingExecutionId ?? listActiveId;
    // The conversation store is shared between useExecutionStream (which
    // ingests snapshots with structural sharing + rAF coalescing) and the
    // rendering tree. This eliminates the duplicate structuralShare that
    // was previously done in this hook.
    const conversationStore = useConversationStoreRef();
    const stream = useExecutionStream(activeExecutionId, {
        store: conversationStore,
    });
    // Re-discovery gate. Poll only while the live stream cannot carry us:
    //  • a fresh session whose first execution is created but not yet listed
    //    (`executions.length === 0`) — the race this fix targets,
    //  • a silent connect-timeout, or an exhausted stream error.
    // Never while the stream is healthy (`isStreaming`) or the active execution
    // has reached a terminal phase — the live feed is then the source of truth.
    const streamTerminal = activeExecutionId !== null && isTerminalPhase(stream.phase);
    const needsRediscovery = !stream.isStreaming &&
        !streamTerminal &&
        ((activeExecutionId === null && executions.length === 0) ||
            stream.connectTimedOut ||
            stream.error !== null);
    useEffect(() => {
        setRediscoveryActive(needsRediscovery);
    }, [needsRediscovery]);
    // Clear pendingExecutionId once the execution appears in the fetched list
    useEffect(() => {
        if (pendingExecutionId &&
            executions.some((e) => (e.metadata?.id ?? "") === pendingExecutionId)) {
            setPendingExecutionId(null);
        }
    }, [pendingExecutionId, executions]);
    // Clear the optimistic message — and any stale send error — once the stream
    // delivers a real snapshot. This also handles recovery: if a failed send's
    // execution is later re-discovered and streams, the failed turn resolves into
    // the live one instead of lingering. (At send time the composer is only
    // enabled when no execution is active, so a *fresh* failure cannot be cleared
    // here prematurely — `stream.execution` is null then.)
    useEffect(() => {
        if (!stream.execution)
            return;
        if (pendingUserMessage)
            setPendingUserMessage(null);
        if (sendError)
            setSendError(null);
    }, [pendingUserMessage, sendError, stream.execution]);
    // Refetch executions when stream reaches a terminal phase so the
    // fetched list reflects the completed status and listActiveId clears.
    useEffect(() => {
        if (activeExecutionId && isTerminalPhase(stream.phase)) {
            refetch();
        }
    }, [activeExecutionId, stream.phase, refetch]);
    const completedExecutions = useMemo(() => {
        if (!activeExecutionId)
            return executions;
        return executions.filter((e) => (e.metadata?.id ?? "") !== activeExecutionId);
    }, [executions, activeExecutionId]);
    const fetchedActiveExecution = useMemo(() => {
        if (!activeExecutionId)
            return null;
        return (executions.find((e) => (e.metadata?.id ?? "") === activeExecutionId) ?? null);
    }, [executions, activeExecutionId]);
    const activeStreamExecution = stream.execution ?? fetchedActiveExecution;
    const activePhase = useMemo(() => {
        if (!activeExecutionId)
            return null;
        return stream.phase;
    }, [activeExecutionId, stream.phase]);
    // Stop is only valid in phases the backend cancels/terminates from
    // (PENDING / IN_PROGRESS). Other non-terminal phases (e.g.
    // WAITING_FOR_APPROVAL) are handled by their own control surface.
    const isStoppable = activePhase === ExecutionPhase.EXECUTION_PENDING ||
        activePhase === ExecutionPhase.EXECUTION_IN_PROGRESS;
    const stopActions = useAgentExecutionActions(activeExecutionId, {
        // The cancel/terminate also broadcasts the new phase over the stream, but
        // refetch is the belt-and-suspenders that clears the active id even if the
        // stream has already ended — mirrors the workflow viewer's onSuccess.
        onSuccess: refetch,
    });
    const stop = useCallback(async (reason) => {
        if (!isStoppable)
            return;
        await stopActions.stop(reason);
    }, [isStoppable, stopActions.stop]);
    const canSendFollowUp = !isCreating && activeExecutionId === null;
    const workspaceEntries = useMemo(() => session?.spec?.workspaceEntries ?? [], [session]);
    const mcpServerUsages = useMemo(() => session?.spec?.mcpServerUsages ?? [], [session]);
    const skillRefs = useMemo(() => session?.spec?.skillRefs ?? [], [session]);
    const sendFollowUp = useCallback(async (message, options) => {
        if (!sessionId || !session)
            return;
        // Capture for retry and clear any prior failure before the new attempt.
        lastSendRef.current = { message, options };
        setSendError(null);
        // A Build-from-plan turn shows no optimistic bubble: its message is a
        // machine-written label, not user prose, and the thread hides the turn
        // entirely (the plan card's "Starting build…" state covers the send
        // window). It is still set on FAILURE below, so a failed build send
        // renders the failed-with-retry bubble instead of vanishing.
        if (!options?.buildFromPlan) {
            setPendingUserMessage(message);
        }
        try {
            const needsSessionUpdate = options?.agentInstanceId !== undefined ||
                options?.workspaceEntries !== undefined ||
                options?.mcpServerUsages !== undefined ||
                options?.skillRefs !== undefined;
            if (needsSessionUpdate) {
                // Fetch the latest session to avoid overwriting fields that were
                // modified server-side (e.g., LLM-generated subject)
                // since the React state was last loaded.
                const freshSession = await stigmer.session.get(sessionId);
                await updateSession(buildUpdateInput(freshSession, {
                    agentInstanceId: options?.agentInstanceId,
                    workspaceEntries: options?.workspaceEntries,
                    mcpServerUsages: options?.mcpServerUsages,
                    skillRefs: options?.skillRefs,
                }));
                refetchSession();
            }
            const result = await create({
                org,
                sessionId,
                message,
                modelName: options?.modelName,
                runtimeEnv: options?.runtimeEnv,
                attachments: options?.attachments,
                interactionMode: options?.interactionMode,
                buildFromPlan: options?.buildFromPlan,
                autoApproveAll: options?.autoApproveAll,
                workspaceFileRefs: options?.workspaceFileRefs,
            });
            setPendingExecutionId(result.executionId);
            refetch();
        }
        catch (err) {
            // Surface the failure and KEEP the user's message visible (do not clear
            // pendingUserMessage) so the turn renders as failed-with-retry instead
            // of vanishing. Covers both the update() and create() paths. For a
            // build turn this is the FIRST time the message is set — failure is
            // the one case where its label must become visible.
            setPendingUserMessage(message);
            setSendError(toError(err));
            if (process.env.NODE_ENV !== "production") {
                console.error("[useSessionConversation] sendFollowUp failed:", err);
            }
        }
    }, [sessionId, session, org, stigmer, create, updateSession, refetch, refetchSession]);
    const retryLastSend = useCallback(() => {
        const last = lastSendRef.current;
        if (last)
            void sendFollowUp(last.message, last.options);
    }, [sendFollowUp]);
    const clearSendError = useCallback(() => {
        setSendError(null);
        clearCreateError();
    }, [clearCreateError]);
    const pendingApprovals = useMemo(() => activeStreamExecution?.status?.pendingApprovals ?? [], [activeStreamExecution]);
    const submitApproval = useCallback(async (toolCallId, action, comment) => {
        if (!activeExecutionId)
            return;
        await rawSubmitApproval(activeExecutionId, toolCallId, action, comment);
    }, [activeExecutionId, rawSubmitApproval]);
    const fileChangeSets = useMemo(() => (activeStreamExecution?.status?.fileChangeSets ?? []).filter((cs) => cs.status === FileChangeSetStatus.AWAITING_REVIEW), [activeStreamExecution]);
    // Mid-run live capture (DD-32): the transient, non-authoritative "N files
    // changed so far" snapshot for the active turn. The server clears it once the
    // turn's change set leaves CAPTURING, so its mere presence means a turn is
    // still accumulating changes. Reference-stable per DD-010 (it rides the
    // structurally-shared live status).
    const fileChangeProgress = useMemo(() => activeStreamExecution?.status?.fileChangeProgress, [activeStreamExecution]);
    const submitFileDecision = useCallback(async (changeSetId, action, options) => {
        if (!activeExecutionId)
            return;
        await rawSubmitFileDecision(activeExecutionId, changeSetId, action, options);
    }, [activeExecutionId, rawSubmitFileDecision]);
    const isLoading = sessionLoading || executionsLoading;
    const loadError = sessionError || executionsError;
    return {
        session,
        completedExecutions,
        activeStreamExecution,
        activePhase,
        isStreaming: stream.isStreaming,
        isConnecting: stream.isConnecting,
        sendFollowUp,
        canSendFollowUp,
        isSending: isCreating,
        sendError,
        clearSendError,
        retryLastSend,
        pendingUserMessage,
        workspaceEntries,
        mcpServerUsages,
        skillRefs,
        pendingApprovals,
        submitApproval,
        submittingApprovalIds: submittingToolCallIds,
        approvalErrors,
        approvalError,
        clearApprovalError,
        fileChangeSets,
        fileChangeProgress,
        submitFileDecision,
        submittingFileDecisionKeys: submittingDecisionKeys,
        fileDecisionErrors,
        clearFileDecisionError,
        fileReviewError,
        clearFileReviewError,
        isStoppable,
        stop,
        isStopping: stopActions.isSubmitting,
        stopError: stopActions.error,
        isLoading,
        loadError,
        isReconnecting: stream.isReconnecting,
        connectTimedOut: stream.connectTimedOut,
        isSlow: stream.isSlow,
        streamError: stream.error,
        reconnectStream: stream.reconnect,
    };
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/**
 * Converts the existing Session proto into a SessionInput suitable for
 * the update RPC (which uses replace semantics — the full spec is sent).
 *
 * For each session-level collection (workspace, MCP servers, skills):
 * if an override is provided, it replaces the existing value; otherwise
 * the current session value is preserved by converting it back to input
 * format.
 */
function buildUpdateInput(session, overrides) {
    const meta = session.metadata;
    const spec = session.spec;
    const workspaceEntries = overrides.workspaceEntries ?? specWorkspaceToInput(spec);
    const mcpServerUsages = overrides.mcpServerUsages ?? specMcpUsagesToInput(spec);
    const skillRefs = overrides.skillRefs ?? specSkillRefsToInput(spec);
    return {
        name: meta.name,
        org: meta.org,
        agentInstanceId: overrides.agentInstanceId ?? (spec?.agentInstanceId || undefined),
        subject: spec?.subject || undefined,
        harnessStateId: spec?.harnessStateId || undefined,
        harness: spec?.harness,
        cursorMode: spec?.cursorMode,
        executionTarget: spec?.executionTarget,
        metadata: spec?.metadata && Object.keys(spec.metadata).length > 0
            ? { ...spec.metadata }
            : undefined,
        workspaceEntries: workspaceEntries?.length ? workspaceEntries : undefined,
        mcpServerUsages: mcpServerUsages?.length ? mcpServerUsages : undefined,
        skillRefs: skillRefs?.length ? skillRefs : undefined,
    };
}
//# sourceMappingURL=useSessionConversation.js.map