import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
/**
 * DataIngestionCheckScreen — "Start your app and trigger some actions" polling screen.
 *
 * Shown after MCP setup. Polls for events every 30 seconds:
 *   1. MCP query_dataset (Bearer auth, works for all users, detects any track() call)
 *   2. App API activation endpoint (autocapture events only, session-cookie auth)
 *   3. Data-API event catalog (taxonomy events as a proxy)
 *
 * Active guidance as prominent instruction with framework-aware hint.
 *
 * Surfaces the agent's tracking plan inline (read from
 * `<installDir>/.amplitude/events.json` via `readLocalEventPlan`) so users
 * remember what they need to trigger. Each planned event is rendered with a
 * status marker that flips from spinner → ✓ as the polling loop accumulates
 * `activeEventNames` from the MCP `get_events` response — same "live arrival"
 * feedback the data-setup phase showed during instrumentation.
 *
 * Skip guard: when zero events have been observed (whether the activation
 * API is healthy or unavailable), pressing Enter or q surfaces a two-step
 * "Continue without verifying? [y] / [Esc] keep waiting" prompt so the user
 * cannot accidentally bypass verification. With partial evidence (MCP
 * observed names or catalog fallback), Enter or q confirms immediately.
 * x exits the wizard to resume later (cancel outro).
 */
import { Box, Text } from 'ink';
import { useEffect, useRef, useState } from 'react';
import { useWizardStore } from '../hooks/useWizardStore.js';
import { Colors, Icons } from '../styles.js';
import { BrailleSpinner } from '../components/BrailleSpinner.js';
import { useScreenInput } from '../hooks/useScreenInput.js';
import { useEscapeBack } from '../hooks/useEscapeBack.js';
import { withTimeout } from '../utils/with-timeout.js';
import { extractAppId, fetchAmplitudeUser, fetchHasAnyEventsMcp, fetchProjectActivationStatus, fetchProjectEventTypes, } from '../../../lib/api.js';
import { DEFAULT_AMPLITUDE_ZONE, Integration } from '../../../lib/constants.js';
import { resolveZone } from '../../../lib/zone-resolution.js';
import { useResolvedZone } from '../hooks/useResolvedZone.js';
import { FRAMEWORK_REGISTRY } from '../../../lib/registry.js';
import { readLocalEventPlan } from '../../../lib/event-plan-parser.js';
import { OutroKind } from '../session-constants.js';
import { logToFile } from '../../../utils/debug.js';
import { detectBoundPort } from '../../../utils/port-detection.js';
import { makeLink } from '../utils/terminal-rendering.js';
const POLL_INTERVAL_MS = 30_000;
const MAX_EVENTS_SHOWN = 8;
const CELEBRATION_DELAY_MS = 3_000;
// Stable identities so useScreenHints' effect doesn't re-fire every render.
const VERIFY_EXTRA_HINTS = Object.freeze([
    { key: 'Enter / q', label: 'Skip verification' },
    { key: 'x', label: 'Exit and resume later' },
]);
const NO_HINTS = Object.freeze([]);
const FRAMEWORK_HINTS = {
    [Integration.nextjs]: {
        kind: 'web',
        ports: [3000, 3001, 3002, 8080],
        running: (url) => _jsxs(_Fragment, { children: ["Visit ", url, " and click around"] }),
        idle: 'Start your dev server, then visit it and click around',
    },
    [Integration.vue]: {
        kind: 'web',
        ports: [5173, 5174, 4173, 3000, 8080],
        running: (url) => _jsxs(_Fragment, { children: ["Visit ", url, " and interact with your app"] }),
        idle: 'Start your dev server, then visit it and interact with your app',
    },
    [Integration.reactRouter]: {
        kind: 'web',
        ports: [3000, 3001, 5173, 8080],
        running: (url) => _jsxs(_Fragment, { children: ["Visit ", url, " and navigate between pages"] }),
        idle: 'Start your dev server, then visit it and navigate between pages',
    },
    [Integration.django]: {
        kind: 'web',
        ports: [8000, 8080, 5000],
        running: (url) => _jsxs(_Fragment, { children: ["Visit ", url, " and browse a few pages"] }),
        idle: 'Start your dev server, then visit it and browse a few pages',
    },
    [Integration.flask]: {
        kind: 'web',
        ports: [5000, 5001, 8000, 8080],
        running: (url) => _jsxs(_Fragment, { children: ["Visit ", url, " and trigger some requests"] }),
        idle: 'Start your dev server, then visit it and trigger some requests',
    },
    [Integration.fastapi]: {
        kind: 'web',
        ports: [8000, 8080, 5000],
        pathSuffix: '/docs',
        running: (url) => _jsxs(_Fragment, { children: ["Visit ", url, " and try some endpoints"] }),
        idle: 'Start your dev server, then visit /docs and try some endpoints',
    },
    [Integration.reactNative]: {
        kind: 'native',
        idle: 'Open your app on a device or emulator and tap around',
    },
    [Integration.swift]: {
        kind: 'native',
        idle: 'Build and run your app, then interact with it',
    },
    [Integration.android]: {
        kind: 'native',
        idle: 'Build and run your app, then interact with it',
    },
    [Integration.flutter]: {
        kind: 'native',
        idle: 'Run your app and navigate through a few screens',
    },
};
/**
 * Frameworks whose runtime is a browser, derived from each framework
 * config's `metadata.targetsBrowser` flag. Used to gate browser-specific
 * coaching tips (Network tab, console) that don't apply to native mobile,
 * server-side, or game-engine runtimes.
 */
const BROWSER_FRAMEWORKS = new Set(Object.values(FRAMEWORK_REGISTRY)
    .filter((config) => config.metadata.targetsBrowser)
    .map((config) => config.metadata.integration));
/**
 * Backend / server-side SDKs. For these integrations the App API
 * activation endpoint is unreliable as a success signal — autocapture is
 * irrelevant and the user has no browser tab to "click around" in. We
 * fall back to the data-API event catalog (taxonomy entries), which is
 * a strong proxy for "ingestion is working" in a backend context where
 * events are typically registered as the SDK initializes.
 *
 * Browser SDKs (nextjs/vue/react-router/javascript_web) intentionally
 * do NOT get this fallback — schema registrations there can predate
 * real ingestion and would falsely celebrate. Mobile / native / game
 * engine integrations are excluded too: the PR's coaching tips are the
 * primary unblock path for them and we don't want to over-trigger.
 */
export const BACKEND_SDK_INTEGRATIONS = new Set([
    Integration.django,
    Integration.flask,
    Integration.fastapi,
    Integration.go,
    Integration.java,
    Integration.javascriptNode,
    Integration.python,
]);
export const DataIngestionCheckScreen = ({ store, }) => {
    useWizardStore(store);
    const { session } = store;
    const { activationLevel } = session;
    const zone = useResolvedZone(session);
    const pollingRef = useRef(null);
    const celebrationTimerRef = useRef(null);
    // Cache a lazily-resolved project ID across poll cycles.
    const resolvedAppIdRef = useRef(null);
    const [apiUnavailable, setApiUnavailable] = useState(false);
    const [eventTypes, setEventTypes] = useState(null);
    const [celebrating, setCelebrating] = useState(false);
    const [celebrationReady, setCelebrationReady] = useState(false);
    const [arrivedEvents, setArrivedEvents] = useState([]);
    const [lastChecked, setLastChecked] = useState(null);
    const [secondsSince, setSecondsSince] = useState(0);
    const [pollingStartTime] = useState(() => Date.now());
    const [elapsedSeconds, setElapsedSeconds] = useState(0);
    const [detectedPort, setDetectedPort] = useState(null);
    /**
     * Tracking plan rendered above the spinner so users can see what they
     * need to trigger. Prefer the live store (populated by the dual-path
     * watcher in `agent-interface.ts` while the agent runs); fall back to
     * reading the file from disk on mount in case the screen is reached on
     * a fresh process where the watcher never set it (re-runs, recovery,
     * activationLevel = 'partial' paths).
     */
    const [plannedEvents, setPlannedEvents] = useState(() => store.eventPlan.length > 0
        ? store.eventPlan
        : readLocalEventPlan(session.installDir));
    /**
     * Names observed across the polling loop. We accumulate (rather than
     * replacing the snapshot from the latest poll) so a transient empty
     * response doesn't visually un-check events the user has already
     * triggered. A Set so we can render the planned-list checkmarks in O(1).
     */
    const [observedEventNames, setObservedEventNames] = useState(() => new Set());
    // Mirror state into a ref so the poll-driven `recordObservedEvents` —
    // captured once by the mount-time `useEffect(() => {...}, [])` and
    // reused by `setInterval` — sees the latest committed set instead of
    // the initial empty one. Without this, every poll's diff was computed
    // against the stale render-0 closure and `mutated` was always `true`.
    const observedEventNamesRef = useRef(observedEventNames);
    useEffect(() => {
        observedEventNamesRef.current = observedEventNames;
    }, [observedEventNames]);
    /**
     * One-shot guard against accidental Enter when the activation API is
     * unavailable AND no events have been observed. First Enter flips this
     * on and renders a confirmation prompt; second Enter (now interpreted
     * as 'y') skips. Esc cancels the prompt and goes back to listening.
     * Cassie's feedback: at-launch verification was too easy to skip past.
     */
    const [awaitingSkipConfirm, setAwaitingSkipConfirm] = useState(false);
    /** Confirm ingestion with a celebration, then wait for user to press Enter. */
    function confirmWithCelebration(events) {
        if (pollingRef.current !== null)
            clearInterval(pollingRef.current);
        setCelebrating(true);
        if (events && events.length > 0)
            setArrivedEvents(events);
        celebrationTimerRef.current = setTimeout(() => {
            setCelebrationReady(true);
        }, CELEBRATION_DELAY_MS);
    }
    /**
     * Merge a poll's `activeEventNames` into the observed set. Returns
     * true if any new events were added (used to log progressive arrivals
     * and to trigger React state updates only when there's actually a
     * change — Set identity matters for `useEffect` deps elsewhere).
     *
     * Two correctness invariants:
     *
     *   1. `mutated` is computed synchronously — React's functional
     *      updater runs during the next render, so reading inside the
     *      setter and returning afterwards always yielded `false`.
     *
     *   2. We read from `observedEventNamesRef.current`, not the
     *      captured-by-render `observedEventNames`. This function is
     *      called from `checkIngestion`, which is itself captured once
     *      by the mount-time `useEffect(() => {...}, [])` and reused
     *      by `setInterval` for the lifetime of the screen — closing
     *      over the render-0 (always-empty) state would mean every poll
     *      thinks all events are new.
     */
    function recordObservedEvents(names) {
        if (names.length === 0)
            return false;
        const current = observedEventNamesRef.current;
        let mutated = false;
        for (const name of names) {
            if (!current.has(name)) {
                mutated = true;
                break;
            }
        }
        if (!mutated)
            return false;
        setObservedEventNames((prev) => {
            const next = new Set(prev);
            for (const name of names)
                next.add(name);
            return next;
        });
        return true;
    }
    /**
     * Silently refresh the OAuth token.
     * Pass force=true to refresh even if the token appears locally valid
     * (e.g. after an auth error — the token may have been server-side revoked).
     */
    async function refreshToken(force = false) {
        try {
            const { getStoredToken, getStoredUser, storeToken } = await import('../../../utils/ampli-settings.js');
            const { refreshAccessToken } = await import('../../../utils/oauth.js');
            const { EXPIRY_BUFFER_MS } = await import('../../../utils/token-refresh.js');
            const user = getStoredUser();
            const stored = getStoredToken(user?.id, user?.zone);
            if (!stored || !user)
                return false;
            // Apply the same 5-minute skew buffer that `tryRefreshToken` uses.
            // Prior to this, the comparison was a raw `now <= expiresAt`, which
            // meant a laptop clock even a few seconds behind would skip refresh
            // and the immediately-following App API call would 401 without retry.
            if (!force &&
                new Date(stored.expiresAt).getTime() - Date.now() > EXPIRY_BUFFER_MS) {
                return false;
            }
            logToFile(`[DataIngestionCheck] refreshing token (force=${force})`);
            const refreshed = await refreshAccessToken(stored.refreshToken, user.zone);
            storeToken(user, {
                accessToken: refreshed.accessToken,
                idToken: refreshed.idToken,
                refreshToken: refreshed.refreshToken,
                expiresAt: refreshed.expiresAt,
            });
            store.updateTokens(refreshed.accessToken, refreshed.idToken);
            logToFile('[DataIngestionCheck] token refreshed successfully');
            return true;
        }
        catch (err) {
            logToFile(`[DataIngestionCheck] token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
            return false;
        }
    }
    async function checkIngestion() {
        // Read from store.session at call time to avoid stale closures
        const currentSession = store.session;
        const currentCredentials = currentSession.credentials;
        if (!currentCredentials) {
            setApiUnavailable(true);
            return;
        }
        // Bail early only if we have no way to identify either the project
        // (via numeric Amplitude app ID — required by both the MCP and App
        // API checks below) OR its org (so the lazy-resolution path below
        // can populate one from /user). Without either, nothing can succeed.
        //
        // CRITICAL: do NOT fall back to selectedProjectId. selectedProjectId
        // is a project UUID, not an app ID; passing a UUID where the App API
        // expects a numeric app ID produces a query that never matches a
        // real app and silently returns zero events forever — leaving users
        // on freshly-typed API keys (where credentials.appId === 0) stuck on
        // this screen until the polling cap. (Pre-fix this was the dominant
        // failure mode for manual key entry.)
        if (!currentCredentials.appId &&
            !currentSession.selectedAppId &&
            !resolvedAppIdRef.current &&
            !currentSession.selectedOrgId) {
            setApiUnavailable(true);
            return;
        }
        // readDisk: false — region is populated on the session by the time this
        // screen renders (flow is gated on region !== null). Skipping Tier 2/3
        // avoids synchronous readAmpliConfig + getStoredUser disk reads every
        // 30s poll.
        const zone = resolveZone(currentSession, DEFAULT_AMPLITUDE_ZONE, {
            readDisk: false,
        });
        const dataApiToken = currentCredentials.idToken ?? currentCredentials.accessToken;
        // Silently refresh if the token has expired before making any API calls.
        await refreshToken();
        // Re-read credentials from the store in case refresh updated them.
        let freshCredentials = store.session.credentials ?? currentCredentials;
        // Step 1: Query via MCP (Bearer auth, works for all users).
        // Uses _all event type so any custom track() calls are detected.
        // Requires the numeric analytics project ID from project.environments[].app.id.
        //
        // selectedAppId may be null if the startup fire-and-forget fetchAmplitudeUser
        // failed (e.g. due to an expired token). Resolve lazily using the now-fresh token.
        //
        // `currentCredentials.appId` is the third source: the `--api-key`
        // + `--app-id` CLI path stamps the numeric app ID directly into
        // credentials and short-circuits OAuth-based resolution, so neither
        // `selectedAppId` nor the lazy `fetchAmplitudeUser` path will populate
        // it. Without this fallback, those users would always hit
        // `setApiUnavailable(true)` despite passing a valid app ID. The
        // bail-out condition above (`!credentials.appId && !selectedAppId
        // && !resolvedAppIdRef.current && !selectedOrgId`) already mirrors
        // this set; keep them in sync so what gets us past the bail-out is
        // the same thing the API call below uses.
        //
        // `credentials.appId` is `AppId | 0` (0 = sentinel for "unknown"),
        // so explicitly check truthiness before stringifying.
        const credentialsAppId = currentCredentials.appId
            ? String(currentCredentials.appId)
            : null;
        let effectiveAppId = currentSession.selectedAppId ??
            resolvedAppIdRef.current ??
            credentialsAppId;
        if (!effectiveAppId) {
            const tryResolve = async () => {
                const userInfo = await fetchAmplitudeUser(freshCredentials.idToken ?? freshCredentials.accessToken, zone);
                // Fall back to the first org if the stored ID doesn't match (stale checkpoint).
                const org = currentSession.selectedOrgId
                    ? userInfo.orgs.find((o) => o.id === currentSession.selectedOrgId) ??
                        userInfo.orgs[0]
                    : userInfo.orgs[0];
                // Fall back to the first project if the stored ID doesn't match.
                const project = org && currentSession.selectedProjectId
                    ? org.projects.find((p) => p.id === currentSession.selectedProjectId) ?? org.projects[0]
                    : org?.projects[0];
                const restoredFields = {};
                if (org && !currentSession.selectedOrgId) {
                    restoredFields.orgId = org.id;
                    restoredFields.orgName = org.name;
                    logToFile(`[DataIngestionCheck] lazily set orgId=${org.id}`);
                }
                if (project && !currentSession.selectedProjectId) {
                    restoredFields.projectId = project.id;
                    restoredFields.projectName = project.name;
                    logToFile(`[DataIngestionCheck] lazily set projectId=${project.id}`);
                }
                effectiveAppId = project ? extractAppId(project) : null;
                if (effectiveAppId) {
                    resolvedAppIdRef.current = effectiveAppId;
                    restoredFields.appId = effectiveAppId;
                    logToFile(`[DataIngestionCheck] lazily resolved appId=${effectiveAppId}`);
                }
                else {
                    logToFile('[DataIngestionCheck] lazy resolution: no environments with app.id');
                }
                if (Object.keys(restoredFields).length > 0) {
                    store.restoreSessionIds(restoredFields);
                }
            };
            try {
                await tryResolve();
            }
            catch (resolveErr) {
                const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr);
                logToFile(`[DataIngestionCheck] lazy resolution failed: ${msg}`);
                if (msg.toLowerCase().includes('auth') ||
                    msg.toLowerCase().includes('401') ||
                    msg.toLowerCase().includes('403')) {
                    const refreshed = await refreshToken(true);
                    if (refreshed) {
                        freshCredentials = store.session.credentials ?? freshCredentials;
                        try {
                            await tryResolve();
                        }
                        catch (retryErr) {
                            logToFile(`[DataIngestionCheck] lazy resolution retry failed: ${retryErr instanceof Error
                                ? retryErr.message
                                : String(retryErr)}`);
                        }
                    }
                }
            }
        }
        if (effectiveAppId) {
            const result = await fetchHasAnyEventsMcp(freshCredentials.accessToken, effectiveAppId);
            logToFile(`[DataIngestionCheck] MCP check: hasEvents=${result.hasEvents} appId=${effectiveAppId} events=${result.activeEventNames.length}`);
            // Accumulate observed event names every poll — even when hasEvents
            // is false (the underlying API returns an empty list in that case,
            // so this is a no-op there but keeps the call site uniform). When
            // hasEvents flips true the merged set drives the live tracking-plan
            // checkmarks; the user sees events tick off as they trigger them.
            const added = recordObservedEvents(result.activeEventNames);
            if (added) {
                logToFile(`[DataIngestionCheck] new events observed: ${result.activeEventNames.join(', ')}`);
            }
            if (result.hasEvents) {
                setApiUnavailable(false);
                confirmWithCelebration(result.activeEventNames);
                return;
            }
        }
        else {
            logToFile('[DataIngestionCheck] MCP check skipped: no appId resolved');
        }
        // Step 2: Try activation status via App API (org-scoped, autocapture events only).
        // Both an orgId and a numeric app ID are required — without the app ID,
        // the call would either 400 or, worse, "succeed" against a wrong identifier
        // and silently return zero events (the pre-fix UUID-as-appId failure mode).
        if (!currentSession.selectedOrgId || !effectiveAppId) {
            setApiUnavailable(true);
            return;
        }
        try {
            const status = await fetchProjectActivationStatus({
                accessToken: currentCredentials.accessToken,
                zone,
                appId: effectiveAppId,
                orgId: currentSession.selectedOrgId,
            });
            setLastChecked(Date.now());
            logToFile(`[DataIngestionCheck] poll result: hasAnyEvents=${status.hasAnyEvents} hasDetSource=${status.hasDetSource}`);
            if (status.hasAnyEvents) {
                confirmWithCelebration();
                return;
            }
            // Do NOT treat the event catalog as a success signal for browser /
            // mobile / engine SDKs — it reflects schema registrations, not real
            // ingestion, and would falsely celebrate.
            //
            // Backend SDKs are different: the activation endpoint is autocapture-
            // only, so a Node/Django/Flask/FastAPI/Go/Java/Python user who's
            // ingesting custom events will look like "no events" forever even
            // when their server is firing. The cataloged event types are a
            // reasonable proxy in that case — if any taxonomy entries exist for
            // their workspace, treat that as the success signal so they aren't
            // stuck on this screen until the outer timeout.
            const integration = currentSession.integration;
            if (integration &&
                BACKEND_SDK_INTEGRATIONS.has(integration) &&
                currentSession.selectedOrgId &&
                currentSession.selectedProjectId) {
                try {
                    const names = await fetchProjectEventTypes(dataApiToken, zone, currentSession.selectedOrgId, currentSession.selectedProjectId);
                    if (names.length > 0) {
                        logToFile(`[DataIngestionCheck] backend SDK catalog fallback: ${names.length} event types found, confirming`);
                        confirmWithCelebration(names);
                        return;
                    }
                }
                catch (catalogErr) {
                    logToFile(`[DataIngestionCheck] backend SDK catalog fallback errored: ${catalogErr instanceof Error
                        ? catalogErr.message
                        : String(catalogErr)}`);
                }
            }
        }
        catch (err) {
            logToFile(`[DataIngestionCheck] poll error: ${err instanceof Error ? err.message : String(err)}`);
            // Clear the polling interval so it stops firing against the failing API
            if (pollingRef.current !== null) {
                clearInterval(pollingRef.current);
                pollingRef.current = null;
            }
            setApiUnavailable(true);
            // Fetch cataloged event types as a proxy for "events arrived". Wrap
            // in a 15s timeout so a hanging data-api request can't leave the user
            // staring at "Checking your event catalog…" forever — on timeout we
            // treat the catalog as empty, which still unblocks the screen
            // (Enter/q skip + x exit hints render once apiUnavailable=true).
            if (currentSession.selectedOrgId && currentSession.selectedProjectId) {
                withTimeout(fetchProjectEventTypes(dataApiToken, zone, currentSession.selectedOrgId, currentSession.selectedProjectId), 15_000, 'event catalog fetch')
                    .then((names) => {
                    setEventTypes(names);
                })
                    .catch((catalogErr) => {
                    logToFile(`[DataIngestionCheck] catalog fetch failed: ${catalogErr instanceof Error
                        ? catalogErr.message
                        : String(catalogErr)}`);
                    setEventTypes([]);
                });
            }
            else {
                setEventTypes([]);
            }
        }
    }
    useEffect(() => {
        // Already fully activated — confirm immediately and skip polling
        if (activationLevel === 'full') {
            store.setDataIngestionConfirmed();
            return;
        }
        void checkIngestion();
        pollingRef.current = setInterval(() => {
            void checkIngestion();
        }, POLL_INTERVAL_MS);
        return () => {
            if (pollingRef.current !== null) {
                clearInterval(pollingRef.current);
            }
            if (celebrationTimerRef.current !== null) {
                clearTimeout(celebrationTimerRef.current);
            }
        };
    }, []);
    // Update seconds-since counter and elapsed timer — every 5s to reduce re-renders
    useEffect(() => {
        const id = setInterval(() => {
            if (lastChecked) {
                setSecondsSince(Math.floor((Date.now() - lastChecked) / 1000));
            }
            setElapsedSeconds(Math.floor((Date.now() - pollingStartTime) / 1000));
        }, 5000);
        return () => clearInterval(id);
    }, [lastChecked, pollingStartTime]);
    // Pick up tracking-plan updates that land AFTER mount. The dual-path
    // watcher in `agent-interface.ts` calls `store.setEventPlan()` when the
    // agent writes `events.json`; if that happens while this screen is
    // already mounted (e.g. an inner-agent re-emit), the initial-state
    // snapshot would be stale. Refresh whenever the store's plan grows or
    // its contents change. We only widen — never overwrite a populated
    // local state with an empty store value, since the agent-runtime
    // watcher tears down between runs.
    useEffect(() => {
        const incoming = store.eventPlan;
        if (incoming.length === 0)
            return;
        setPlannedEvents((prev) => {
            if (prev.length === 0)
                return incoming;
            // Same length AND every name matches → nothing changed.
            if (prev.length === incoming.length) {
                const prevNames = new Set(prev.map((e) => e.name));
                if (incoming.every((e) => prevNames.has(e.name)))
                    return prev;
            }
            return incoming;
        });
    }, [store.eventPlan]);
    // Detect a bound dev-server port for web frameworks, then poll every 10s
    // so the hint switches to the live URL as soon as the user starts their app.
    // Only match ports whose listener is running out of the install dir — a
    // raw :3000 scan would also catch unrelated Docker/other-project services.
    useEffect(() => {
        const hint = session.integration
            ? FRAMEWORK_HINTS[session.integration]
            : undefined;
        if (!hint || hint.kind !== 'web')
            return;
        const installDir = session.installDir;
        let cancelled = false;
        const probe = async () => {
            const port = await detectBoundPort(hint.ports, { cwd: installDir });
            if (!cancelled)
                setDetectedPort(port);
        };
        void probe();
        const id = setInterval(() => void probe(), 10_000);
        return () => {
            cancelled = true;
            clearInterval(id);
        };
    }, [session.integration, session.installDir]);
    // Esc → step back to MCP install. Enter/q → skip verification (with guard
    // when there is no evidence). x → cancel outro / exit to resume later.
    // Hidden during celebration so accidental Esc does not undo the run.
    // While the skip-confirm prompt is up, Esc dismisses only the prompt;
    // useEscapeBack is disabled so it does not also call store.goBack().
    useEscapeBack(store, {
        enabled: !celebrating && !awaitingSkipConfirm,
        extraHints: celebrating || awaitingSkipConfirm ? NO_HINTS : VERIFY_EXTRA_HINTS,
    });
    useScreenInput((_char, key) => {
        // During celebration, wait for Enter to advance
        if (celebrating) {
            if (celebrationReady && key.return) {
                store.setDataIngestionConfirmed();
            }
            return;
        }
        // Enter-guard: if the user is staring at the skip-confirm prompt,
        // the same keys that opened it (Enter, q) plus y confirm the skip;
        // anything else dismisses it. Re-pressing Enter/q must not toggle the
        // prompt off — users expect the trigger key to confirm.
        if (awaitingSkipConfirm) {
            if (_char === 'y' ||
                _char === 'Y' ||
                _char === 'q' ||
                _char === 'Q' ||
                key.return) {
                if (pollingRef.current !== null)
                    clearInterval(pollingRef.current);
                store.setDataIngestionConfirmed();
                return;
            }
            // Any other key (e.g. Esc, n) dismisses the prompt.
            setAwaitingSkipConfirm(false);
            return;
        }
        // x → exit wizard and resume later (cancel outro).
        if (_char === 'x' || _char === 'X') {
            if (pollingRef.current !== null)
                clearInterval(pollingRef.current);
            store.setOutroData({
                kind: OutroKind.Cancel,
                message: 'Come back once your app is running and sending events. Your SDK is installed — just trigger some actions.',
            });
            return;
        }
        const hasAnyEvidence = observedEventNames.size > 0 ||
            (eventTypes !== null && eventTypes.length > 0);
        const wantsSkipVerification = key.return || _char === 'q' || _char === 'Q';
        // Enter or q: advance past verification, with the same two-step guard
        // whether the activation API is up (listening) or unavailable (catalog
        // fallback path).
        if (wantsSkipVerification) {
            if (!hasAnyEvidence) {
                setAwaitingSkipConfirm(true);
                return;
            }
            if (pollingRef.current !== null)
                clearInterval(pollingRef.current);
            store.setDataIngestionConfirmed();
        }
    });
    // Derive framework-specific hint. For web frameworks, if `lsof` found a
    // bound port we render a clickable URL; otherwise show the idle message.
    const hintConfig = session.integration
        ? FRAMEWORK_HINTS[session.integration]
        : undefined;
    const frameworkHint = (() => {
        if (!hintConfig)
            return null;
        if (hintConfig.kind === 'native')
            return hintConfig.idle;
        if (detectedPort === null)
            return hintConfig.idle;
        const url = `http://localhost:${detectedPort}${hintConfig.pathSuffix ?? ''}`;
        return hintConfig.running(makeLink(url, url));
    })();
    // Gate browser-specific coaching tips — suppressed for mobile / server /
    // engine frameworks whose runtime has no browser devtools. A Swift /
    // Django / Go user can't "check the Network tab."
    const isBrowserFramework = session.integration
        ? BROWSER_FRAMEWORKS.has(session.integration)
        : false;
    const shown = eventTypes?.slice(0, MAX_EVENTS_SHOWN) ?? [];
    const overflow = (eventTypes?.length ?? 0) - MAX_EVENTS_SHOWN;
    // Tracking-plan rendering: sort observed events first so the user's
    // positive signal is always visible at the top, then show pending
    // events below. Only the first MAX_EVENTS_SHOWN render — the overflow
    // line keeps small terminals from wrapping into garbage.
    const observedCount = plannedEvents.filter((e) => observedEventNames.has(e.name)).length;
    const plannedSorted = observedCount > 0
        ? [
            ...plannedEvents.filter((e) => observedEventNames.has(e.name)),
            ...plannedEvents.filter((e) => !observedEventNames.has(e.name)),
        ]
        : plannedEvents;
    const plannedShown = plannedSorted.slice(0, MAX_EVENTS_SHOWN);
    const plannedOverflow = plannedEvents.length - MAX_EVENTS_SHOWN;
    // ── Celebration state ──────────────────────────────────────────────────
    if (celebrating) {
        // Reconcile the celebration against the tracking plan when we have
        // one. If every planned event has been observed, render the full
        // plan with checkmarks (positive completion signal). If the plan is
        // partial (some still pending), render the same checked/pending
        // split as the waiting state so the user can see exactly what's
        // missing — they may want to keep waiting before pressing Enter.
        // No tracking plan? Fall back to the legacy flat list of arrived
        // event names so we don't regress for legacy runs.
        const hasPlan = plannedEvents.length > 0;
        const allPlannedObserved = hasPlan && observedCount === plannedEvents.length;
        return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, color: Colors.success, children: [Icons.checkmark, ' ', allPlannedObserved
                            ? 'All planned events detected!'
                            : 'Events detected!'] }), hasPlan ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [plannedShown.map((event) => {
                            const observed = observedEventNames.has(event.name);
                            return (_jsxs(Box, { gap: 1, children: [observed ? (_jsx(Text, { color: Colors.success, children: Icons.checkmark })) : (_jsx(Text, { color: Colors.muted, children: Icons.diamond })), _jsx(Text, { color: observed ? Colors.success : Colors.body, children: event.name })] }, event.name));
                        }), plannedOverflow > 0 && (_jsxs(Text, { color: Colors.muted, children: [Icons.ellipsis, " and ", plannedOverflow, " more"] }))] })) : (arrivedEvents.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [arrivedEvents.slice(0, MAX_EVENTS_SHOWN).map((name) => (_jsxs(Text, { color: Colors.success, children: [Icons.diamond, " ", name] }, name))), arrivedEvents.length > MAX_EVENTS_SHOWN && (_jsxs(Text, { color: Colors.muted, children: [Icons.ellipsis, " and ", arrivedEvents.length - MAX_EVENTS_SHOWN, ' ', "more"] }))] }))), _jsx(Box, { marginTop: 1, children: celebrationReady ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: Colors.muted, children: "[" }), _jsx(Text, { color: Colors.body, bold: true, children: "Enter" }), _jsx(Text, { color: Colors.muted, children: "] Continue" })] })) : (_jsxs(Text, { color: Colors.body, children: ["Verifying", Icons.ellipsis] })) })] }));
    }
    // ── Waiting state ──────────────────────────────────────────────────────
    return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: Colors.heading, children: "Start your app and trigger some user actions" }) }), frameworkHint && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: Colors.accent, children: [Icons.arrowRight, " ", frameworkHint] }) })), _jsx(Text, { color: Colors.body, children: "Your SDK is installed. Once you interact with your app, events will start flowing into Amplitude." }), plannedEvents.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: Colors.secondary, children: observedCount > 0
                            ? `Tracking plan (${observedCount} of ${plannedEvents.length} observed):`
                            : `Tracking plan (${plannedEvents.length} ${plannedEvents.length === 1 ? 'event' : 'events'}) — trigger any of these:` }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [plannedShown.map((event) => {
                                const observed = observedEventNames.has(event.name);
                                return (_jsxs(Box, { gap: 1, children: [observed ? (_jsx(Text, { color: Colors.success, children: Icons.checkmark })) : (_jsx(Text, { color: Colors.muted, children: Icons.diamond })), _jsx(Text, { color: observed ? Colors.success : Colors.body, children: event.name })] }, event.name));
                            }), plannedOverflow > 0 && (_jsxs(Text, { color: Colors.muted, children: [Icons.ellipsis, " and ", plannedOverflow, " more"] }))] })] })), !session.agent && (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { color: Colors.secondary, children: [Icons.arrowRight, " If your dev server or build was already running, restart it so the new env values load."] }) })), !apiUnavailable && (_jsxs(Box, { marginTop: 1, gap: 1, alignItems: "center", children: [_jsx(BrailleSpinner, { color: Colors.accent }), _jsxs(Text, { color: Colors.secondary, children: ["Listening for events", Icons.ellipsis, lastChecked && (_jsxs(Text, { color: Colors.muted, children: [" (checked ", secondsSince, "s ago)"] }))] })] })), !apiUnavailable && !celebrating && elapsedSeconds >= 60 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsxs(Text, { color: Colors.secondary, children: [Icons.arrowRight, " Make sure your dev server is running and you've clicked around the app"] }), elapsedSeconds >= 120 && isBrowserFramework && (_jsxs(Text, { color: Colors.secondary, children: [Icons.arrowRight, " In browser devtools, check the Network tab for requests to", ' ', zone === 'eu' ? 'api.eu.amplitude.com' : 'api2.amplitude.com', " \u2014 a 4xx response or a blocked request usually means the SDK isn't sending"] })), elapsedSeconds >= 180 && isBrowserFramework && (_jsxs(Text, { color: Colors.secondary, children: [Icons.arrowRight, " Look in the browser console for errors from", ' ', "@amplitude/* \u2014 a silent init failure blocks all events"] })), elapsedSeconds >= 120 && !isBrowserFramework && (_jsxs(Text, { color: Colors.secondary, children: [Icons.arrowRight, " Check your app's logs for SDK init errors or failed network calls to", ' ', zone === 'eu' ? 'api.eu.amplitude.com' : 'api2.amplitude.com', " \u2014 a silent failure there blocks events"] }))] })), apiUnavailable && eventTypes === null && (_jsxs(Box, { marginTop: 1, gap: 1, alignItems: "center", children: [_jsx(BrailleSpinner, { color: Colors.accent }), _jsxs(Text, { color: Colors.secondary, children: ["Checking your event catalog", Icons.ellipsis] })] })), apiUnavailable && eventTypes !== null && eventTypes.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: Colors.secondary, children: "Events cataloged in your project:" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [shown.map((name) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: Colors.success, children: Icons.diamond }), _jsx(Text, { color: Colors.body, children: name })] }, name))), overflow > 0 && (_jsxs(Text, { color: Colors.muted, children: [Icons.ellipsis, " and ", overflow, " more"] }))] })] })), apiUnavailable && eventTypes !== null && eventTypes.length === 0 && (_jsxs(Box, { marginTop: 1, gap: 1, alignItems: "center", children: [_jsx(BrailleSpinner, { color: Colors.accent }), _jsxs(Text, { color: Colors.secondary, children: ["Waiting for events", Icons.ellipsis] })] })), awaitingSkipConfirm && (_jsxs(Box, { flexDirection: "column", marginTop: 2, children: [_jsxs(Text, { color: Colors.warning, children: [Icons.arrowRight, " No events detected yet. Continue without verifying?"] }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: Colors.muted, children: "[" }), _jsx(Text, { color: Colors.body, bold: true, children: "y" }), _jsx(Text, { color: Colors.muted, children: "] Yes, skip verification" })] }), _jsxs(Box, { children: [_jsx(Text, { color: Colors.muted, children: "[" }), _jsx(Text, { color: Colors.body, bold: true, children: "Esc" }), _jsx(Text, { color: Colors.muted, children: "] Keep waiting" })] })] })] })), !awaitingSkipConfirm && (_jsxs(Box, { marginTop: 2, gap: 2, flexDirection: "row", flexWrap: "wrap", children: [_jsxs(Box, { gap: 0, children: [_jsx(Text, { color: Colors.muted, children: "[" }), _jsx(Text, { color: Colors.body, bold: true, children: "Enter" }), _jsx(Text, { color: Colors.muted, children: "] or [" }), _jsx(Text, { color: Colors.body, bold: true, children: "q" }), _jsx(Text, { color: Colors.muted, children: "] Skip verification" })] }), _jsxs(Box, { gap: 0, children: [_jsx(Text, { color: Colors.muted, children: "[" }), _jsx(Text, { color: Colors.body, bold: true, children: "x" }), _jsx(Text, { color: Colors.muted, children: "] Exit and resume later" })] })] }))] }));
};
