/**
 * InkUI — Ink-backed implementation of WizardUI.
 *
 * Translates business logic calls into store setter calls.
 * No direct session mutation. No imperative screen transitions.
 * The router derives the active screen from session state.
 */
import { toCredentialAppId } from '../../lib/wizard-session.js';
import { Overlay } from './router.js';
import { RunPhase, OutroKind } from './session-constants.js';
// Strip ANSI escape codes (chalk formatting) from strings
// eslint-disable-next-line no-control-regex
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function stripAnsi(s) {
    return s.replace(ANSI_RE, '');
}
export class InkUI {
    store;
    constructor(store) {
        this.store = store;
    }
    intro(message) {
        this.store.pushStatus(message);
    }
    outro(message) {
        this.store.pushStatus(stripAnsi(message));
        if (!this.store.session.outroData) {
            this.store.setOutroData({
                kind: OutroKind.Success,
                message: stripAnsi(message),
            });
        }
        // Signal that the main work is done — router resolves to mcp or outro
        if (this.store.session.runPhase === RunPhase.Running) {
            this.store.setRunPhase(RunPhase.Completed);
        }
    }
    setCredentials(credentials) {
        // The store-level WizardSession.credentials type only carries the four
        // core fields; org/project names live elsewhere on the session. Scope
        // fields here are for the NDJSON layer only — the TUI path ignores them.
        this.store.setCredentials({
            accessToken: credentials.accessToken,
            projectApiKey: credentials.projectApiKey,
            host: credentials.host,
            // Re-validate at the trust boundary: the upstream NDJSON contract
            // accepts a raw `number`, but the store type is `AppId | 0`.
            appId: toCredentialAppId(credentials.appId),
        });
    }
    setDetectedFramework(label) {
        this.store.setDetectedFramework(label);
    }
    onEnterScreen(screen, fn) {
        this.store.onEnterScreen(screen, fn);
    }
    setLoginUrl(url) {
        this.store.setLoginUrl(url);
    }
    setRegion(region) {
        this.store.setRegion(region);
    }
    setProjectHasData(value) {
        this.store.setProjectHasData(value);
    }
    showServiceStatus(data) {
        this.store.setServiceStatus(data);
        this.store.pushOverlay(Overlay.Outage);
    }
    setRetryState(state) {
        this.store.setRetryState(state);
    }
    startRun() {
        this.store.setRunPhase(RunPhase.Running);
    }
    async setRunError(error) {
        this.store.setScreenError(error);
        await this.store.waitForRetry();
        return true;
    }
    setOutroData(data) {
        this.store.setOutroData(data);
    }
    async cancel(message, options) {
        this.store.pushStatus(stripAnsi(message));
        if (!this.store.session.outroData) {
            this.store.setOutroData({
                kind: OutroKind.Cancel,
                message: stripAnsi(message),
                docsUrl: options?.docsUrl,
            });
        }
        else {
            // Re-emit existing outroData so subscribers (OutroScreen) re-render.
            // Business-logic callers that direct-mutate `session.outroData` don't
            // fire change events, so without this the OutroScreen could miss the
            // updated state when cancel() is called immediately after.
            this.store.setOutroData(this.store.session.outroData);
        }
        // Advance past Run screen (RunPhase.Error also skips MCP screen)
        if (this.store.session.runPhase === RunPhase.Running ||
            this.store.session.runPhase === RunPhase.Idle) {
            this.store.setRunPhase(RunPhase.Error);
        }
        // Block until the user dismisses the OutroScreen (or a safety
        // timeout fires). Without this, wizardAbort would call process.exit
        // before Ink rendered the next frame and the user would never see
        // the cancel/error message — they'd just get a half-rendered status
        // banner and a sudden process death.
        //
        // Safety timeout exists because the TUI can theoretically deadlock
        // (e.g. an error during render itself). 5 minutes is generous —
        // long enough for a human to read the bug-report instructions and
        // open the log file, short enough that an unattended CI run that
        // somehow reaches this path doesn't hang forever.
        const SAFETY_TIMEOUT_MS = 5 * 60 * 1000;
        let timer;
        const timeoutPromise = new Promise((resolve) => {
            timer = setTimeout(resolve, SAFETY_TIMEOUT_MS);
        });
        try {
            await Promise.race([this.store.outroDismissed(), timeoutPromise]);
        }
        finally {
            if (timer)
                clearTimeout(timer);
        }
    }
    log = {
        info: (message) => {
            this.store.pushStatus(message);
        },
        warn: (message) => {
            this.store.pushStatus(message);
        },
        error: (message) => {
            this.store.pushStatus(message);
        },
        success: (message) => {
            this.store.pushStatus(message);
        },
        step: (message) => {
            this.store.pushStatus(message);
        },
    };
    note(message) {
        this.store.pushStatus(message);
    }
    spinner() {
        return {
            start: (message) => {
                if (message)
                    this.store.pushStatus(message);
            },
            stop: (message) => {
                if (message)
                    this.store.pushStatus(message);
            },
            message: (msg) => {
                if (msg)
                    this.store.pushStatus(msg);
            },
        };
    }
    seedPostAgentSteps(steps) {
        this.store.seedPostAgentSteps(steps);
    }
    setPostAgentStep(id, patch) {
        this.store.setPostAgentStep(id, patch);
    }
    pushStatus(message) {
        this.store.pushStatus(message);
    }
    heartbeat(_data) {
        // TUI already shows live status updates reactively via pushStatus — no-op
    }
    promptConfirm(message) {
        return this.store.promptConfirm(message);
    }
    promptChoice(message, options) {
        return this.store.promptChoice(message, options);
    }
    promptEventPlan(events) {
        return this.store.promptEventPlan(events);
    }
    syncTodos(todos) {
        this.store.syncTodos(todos);
    }
    applyJourneyTransition(stepId, status) {
        this.store.applyJourneyTransition(stepId, status);
    }
    recordFileChangePlanned(data) {
        this.store.recordFileChangePlanned(data);
    }
    recordFileChangeApplied(data) {
        this.store.recordFileChangeApplied(data);
    }
    setEventPlan(events) {
        this.store.setEventPlan(events);
    }
    setEventIngestionDetected(_eventNames) {
        // In TUI mode, DataIngestionCheckScreen handles this via polling — no-op here.
    }
    setDashboardUrl(url) {
        this.store.setChecklistDashboardUrl(url);
    }
}
