/**
 * WIZ.3 + WAB-SUB.3 — interactive chat-setup wizard. Implements
 * `docs/tasks/WIZ.1-flow.md` storyboard with the WAB-SUB.3 mode-choice
 * extension: detection → mode choice (api | subscription) → mode-specific
 * alias setup → chat_agent.yaml authoring → dry-run preview → confirm →
 * atomic write with backup → outro.
 *
 * Audit invariants:
 *   - No write without explicit `confirm()` → true after dry-run preview.
 *   - `isCancel()` checked after EVERY prompt; cancel path emits a "nothing
 *     written" message and exits clean.
 *   - Secrets prompted via `password()` (masked) IN api mode only; subscription
 *     mode never prompts for a key. Dry-run preview shows `=…<last4>` only.
 *   - Both `mode=api` and `mode=subscription` are first-class choices —
 *     NEITHER pre-selected as `initialValue` (WAB-SUB.3 spec criterion A).
 *   - Existing config (idempotency branch) treats BOTH modes as valid;
 *     Replace walks the mode-choice prompt again.
 *   - `OPENSQUID_NO_BILLED_CALLS=1` skips step (f) entirely.
 *   - `~/.opensquid/setup-chat.lock` via proper-lockfile prevents concurrent runs.
 *
 * Writes delegated to `./chat_actions_writers.ts`; prompt sub-flows to
 * `./chat_actions_prompts.ts`. This file orchestrates only.
 */
import { randomUUID } from 'node:crypto';
import { mkdir, readFile, stat } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { cancel, confirm, intro, isCancel, note, outro, select } from '@clack/prompts';
import pc from 'picocolors';
import { lock as acquireLock } from 'proper-lockfile';
import { OPENSQUID_HOME, resolveProjectUuid } from '../../runtime/paths.js';
import { channelsConfigPath } from '../../channels/routing.js';
import { runIdempotencyBranch, runModeChoice, runModelAliasPrompts, runPackPrompts, } from './chat_actions_prompts.js';
import { runChannelTestStep } from './chat_actions_test_step.js';
import { buildPlan, executePlan, renderPlanPreview, } from './chat_actions_writers.js';
import { defaultEnvPath, detectAgentBridgeRunning, detectChatDaemonRunning, detectModelsConfig, detectPacksDir, detectSecretsBackend, } from './chat_state.js';
// ---------------------------------------------------------------------------
// runChatSetupWizard — top-level orchestrator (lock + intro + branch routing).
// ---------------------------------------------------------------------------
export async function runChatSetupWizard(deps = {}) {
    const setExitCode = deps.setExitCode ?? ((c) => (process.exitCode = c));
    const homeDir = deps.opensquidHome ?? OPENSQUID_HOME();
    const envPath = deps.envPath ?? defaultEnvPath();
    const dryRun = deps.dryRun === true;
    const replace = deps.replace === true;
    const projectCwd = deps.projectCwd ?? process.cwd();
    const projectEnv = deps.projectEnv ?? process.env;
    await mkdir(homeDir, { recursive: true });
    const lockPath = join(homeDir, 'setup-chat.lock');
    let release = null;
    try {
        release = await acquireLock(lockPath, { retries: 0, stale: 30000, realpath: false });
    }
    catch {
        intro(pc.cyan('opensquid setup chat'));
        cancel(`Another opensquid setup chat is running. Refusing to start a second wizard.\nIf the prior run died, remove ${lockPath}.lock and re-run.`);
        setExitCode(1);
        return { outcome: 'concurrent_lock' };
    }
    try {
        return await runInner({
            homeDir,
            envPath,
            setExitCode,
            dryRun,
            replace,
            projectCwd,
            projectEnv,
        });
    }
    finally {
        if (release) {
            try {
                await release();
            }
            catch {
                /* lock-release best-effort */
            }
        }
    }
}
async function runInner(d) {
    // (a) Intro splash
    intro(pc.cyan('opensquid setup chat — guided chat-agent setup'));
    note(introBody(d), 'About');
    // (b) Detection
    const detection = await runDetection(d);
    note(renderDetection(detection), 'State');
    if (detection.models.parseError !== undefined) {
        cancel(parseErrorMessage(detection.models));
        d.setExitCode(2);
        return { outcome: 'aborted' };
    }
    // (i) Idempotency
    // WIZ.5: `--replace` skips the idempotency branch entirely — proceed
    // straight to authoring fresh config, overwriting any existing fast_chat
    // alias. Caller still sees the dry-run preview + confirm before any
    // write happens, so this is not a "silent overwrite" foot-gun.
    if (detection.models.hasFastChat && !d.replace) {
        const choice = await runIdempotencyBranch(detection.models);
        if (choice === 'cancel')
            return abortNoChanges();
        if (choice === 'keep') {
            outro(pc.green('No changes. Existing fast_chat alias preserved.'));
            return { outcome: 'no_changes' };
        }
        if (choice === 'test_only') {
            outro(pc.yellow('Test-only mode is wired in WIZ.4. Re-run after WIZ.4 ships, or pick Replace to re-author.'));
            return { outcome: 'no_changes' };
        }
        // 'replace' falls through.
    }
    else if (detection.models.hasFastChat && d.replace) {
        note('fast_chat alias exists. --replace: skipping confirm, will overwrite after dry-run preview.', 'Replace');
    }
    // (c.0) Mode choice — api vs subscription. WAB-SUB.3: this is the FIRST
    // shape-determining prompt. NEITHER mode is pre-selected; the user
    // explicitly picks. Reached on clean-state runs AND on the Replace branch
    // of an existing-config run (so "Replace" can swap api↔subscription
    // both ways, not just sub→api as the WIZ.3 hard-block forced).
    const mode = await runModeChoice();
    if (mode === 'cancel')
        return abortNoChanges();
    // (c) Model alias setup — branches on the mode choice. api prompts
    //     model + masked key + key dest. subscription prompts cli + impl
    //     + args (no Anthropic API key prompt; subscription auth flows
    //     through the host's own login state).
    const aliasResult = await runModelAliasPrompts(detection.secrets, mode);
    if (aliasResult === null)
        return abortNoChanges();
    // (d) Pack + chat_agent.yaml authoring
    const packResult = await runPackPrompts(detection.packs, d.homeDir);
    if (packResult === null)
        return abortNoChanges();
    // (e) Channel offer — pre-write channel-token detection only. Live
    //     delivery happens post-write in step (f) below; this prompt stays
    //     here so the user sees the channel status before committing writes.
    if ((await runChannelOffer(detection.secrets)) === 'cancel')
        return abortNoChanges();
    // (g) Dry-run preview + confirm + write
    // T-FIX-FIRST-RUN-SETUP A: mint .opensquid/project.json when the FULL
    // resolution (env first, then the cwd-walk — the agent-bridge's own chain)
    // finds no project identity. An env uuid or an existing card anywhere up
    // the walk suppresses creation entirely (suppression IS the idempotency).
    const existingProjectUuid = await resolveProjectUuid({ cwd: d.projectCwd, env: d.projectEnv });
    // FRS.B: on explicit consent, merge the chosen pack into user-scope
    // active.json via the plan (tolerant READ here; buildActiveJson owns the
    // merge; executePlan backs up any existing file before replacing).
    let activeInput = {};
    if (packResult.activatePack) {
        const activePath = join(d.homeDir, 'active.json');
        let existing = [];
        try {
            const parsed = JSON.parse(await readFile(activePath, 'utf8'));
            if (Array.isArray(parsed.packs)) {
                existing = parsed.packs.filter((x) => typeof x === 'string');
            }
        }
        catch {
            /* ENOENT or malformed → empty; the plan's backup covers replacement */
        }
        activeInput = { activatePack: { path: activePath, existing, packId: packResult.packId } };
    }
    // FRS.C: seed minimal channels.json ONLY when truly absent (file-EXISTENCE
    // check — loadChannelsConfig nulls on malformed too, and a malformed live
    // config is the doctor's territory, never the wizard's).
    let channelsInput = {};
    {
        const chPath = channelsConfigPath();
        let exists = true;
        try {
            await stat(chPath);
        }
        catch {
            exists = false;
        }
        if (!exists) {
            const base = basename(d.projectCwd);
            channelsInput = {
                channelsSeed: {
                    path: chPath,
                    // routing.ts:71-77 forbids the reserved "general" umbrella id.
                    umbrellaId: base === 'general' ? 'general-project' : base,
                    memberPath: d.projectCwd,
                },
            };
        }
    }
    const plan = buildPlan({
        ...activeInput,
        ...channelsInput,
        ...(existingProjectUuid === null
            ? {
                projectCard: {
                    path: join(d.projectCwd, '.opensquid', 'project.json'),
                    id: basename(d.projectCwd),
                    uuid: randomUUID(),
                },
            }
            : {}),
        homeDir: d.homeDir,
        envPath: d.envPath,
        modelsState: detection.models,
        fastChatAlias: aliasResult.alias,
        apiKey: aliasResult.apiKey,
        storeKey: aliasResult.storeKey,
        packRoot: packResult.packRoot,
        chatAgent: packResult.chatAgent,
        createPackManifest: packResult.createPackManifest,
        packId: packResult.packId,
        ...(packResult.customPromptPath !== undefined && {
            customPromptPath: packResult.customPromptPath,
        }),
        ...(packResult.customPromptBody !== undefined && {
            customPromptBody: packResult.customPromptBody,
        }),
    });
    note(renderPlanPreview(plan), 'Plan');
    // WIZ.5: `--dry-run` short-circuits before the confirm + executePlan.
    // The plan preview IS the deliverable; nothing is written. We still emit
    // a clear outro so the user knows the wizard finished cleanly.
    if (d.dryRun) {
        outro(pc.yellow(`Dry-run complete. ${String(plan.actions.length)} file(s) WOULD be written.\nRe-run without --dry-run to apply.`));
        return { outcome: 'dry_run' };
    }
    const proceed = await confirm({
        message: `Write these ${String(plan.actions.length)} files?`,
        initialValue: false,
    });
    if (isCancel(proceed) || proceed !== true)
        return abortNoChanges();
    try {
        const result = await executePlan(plan);
        // (e2) Topic creation is no longer a wizard step (CAT.8 retire). The
        //      umbrella's one forum topic is created on the first SessionStart by the
        //      `ensure_umbrella_topic` assurance (default-discipline's
        //      session-connection-check skill) on `channels.json` — the legacy
        //      chat-routing.json `resolveOrCreateTopic` path is gone.
        // (f) WIZ.4 — opt-in live test, post-write. The user's models.yaml +
        //     chat_agent.yaml are on disk by now; offering the test here lets
        //     them verify end-to-end delivery against the freshly written
        //     config. Skipped when OPENSQUID_NO_BILLED_CALLS=1.
        await runChannelTestStep({ daemonState: detection.daemon });
        outro(successOutro(plan));
        return { outcome: 'completed', written: result.written };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        cancel(`Write failed: ${message}\nBackups restored from ${plan.backupDir} (if any existing files were backed up).`);
        d.setExitCode(3);
        return { outcome: 'aborted' };
    }
}
function abortNoChanges() {
    cancel('Aborted. No files modified.');
    return { outcome: 'aborted' };
}
// ---------------------------------------------------------------------------
// Static text builders
// ---------------------------------------------------------------------------
const introBody = (d) => `This wizard walks you through:
  1. Picking how the agent reaches its LLM (Anthropic API key OR Claude Code subscription)
  2. Declaring a model alias (fast_chat) for the agent
  3. Writing a chat_agent.yaml side-file inside one of your packs
  4. (Optional) Sending a test message to verify end-to-end delivery

Nothing is written until you confirm. Ctrl-C aborts safely at any prompt.
Files affected (preview shown before write):
  - ${d.homeDir}/models.yaml       (created or merged)
  - ${d.envPath}                   (appended — ANTHROPIC_API_KEY, api mode only)
  - <pack-root>/chat_agent.yaml    (created or replaced)
  - ${d.homeDir}/backup/<ts>/      (backups of overwritten files)`;
const parseErrorMessage = (models) => `Your existing ${models.path} has a YAML/schema error:
  ${models.parseError ?? 'unknown'}

The wizard refuses to overwrite a file it can't parse, because the merge step needs
to preserve unrelated aliases.

Fix: repair the file manually, OR move it aside
  mv ${models.path} ${models.path}.broken
and re-run.`;
const successOutro = (plan) => {
    // T-FIX-FIRST-RUN-SETUP A: surface a freshly minted project card.
    const card = plan.actions.find((a) => a.path.endsWith(join('.opensquid', 'project.json')));
    return `${pc.green('Chat agent configured.')}${card !== undefined ? `\nCreated ${card.path} (project identity).` : ''}

Next steps:
  - Start the bridge:    opensquid agent-bridge start
  - Test in chat:        send a message to your project Telegram channel
  - Undo this setup:     restore files from ${plan.backupDir}`;
};
async function runDetection(d) {
    const modelsPath = join(d.homeDir, 'models.yaml');
    const packsDir = join(d.homeDir, 'packs');
    const [models, packs, secrets, daemon, bridge] = await Promise.all([
        detectModelsConfig(modelsPath),
        detectPacksDir(packsDir),
        detectSecretsBackend({ envPath: d.envPath }),
        detectChatDaemonRunning(),
        detectAgentBridgeRunning(),
    ]);
    return { models, packs, secrets, daemon, bridge };
}
function renderDetection(det) {
    const y = pc.green;
    const n = pc.dim;
    const b = pc.red;
    const withChat = det.packs.packs.filter((p) => p.hasChatAgent).map((p) => p.name);
    const fastChat = det.models.parseError !== undefined
        ? b('PARSE ERROR')
        : det.models.hasFastChat
            ? y(`present (mode=${det.models.fastChatMode ?? 'unknown'})`)
            : n('absent');
    const modelsState = det.models.parseError !== undefined
        ? b('PARSE ERROR')
        : det.models.present
            ? y('found')
            : n('not found');
    const packCount = det.packs.packs.length > 0
        ? y(`${String(det.packs.packs.length)} pack(s) found`)
        : n('none — wizard will create starter');
    return [
        `Models config (${det.models.path}): ${modelsState}`,
        `  fast_chat alias: ${fastChat}`,
        `Secrets backend (${det.secrets.envPath}): ${det.secrets.envPresent ? y('found') : n('not found')}`,
        `  ANTHROPIC_API_KEY: ${det.secrets.anthropicKeyPresent ? y('set') : n('not set')}`,
        `Packs available (${det.packs.path}): ${packCount}`,
        `  Packs with chat_agent.yaml: ${withChat.length > 0 ? y(withChat.join(', ')) : n('none')}`,
        `chat-daemon running: ${det.daemon.running ? y(`yes (pid=${String(det.daemon.pid)})`) : n('no')}`,
        `agent-bridge running: ${det.bridge.running ? y(`yes (pid=${String(det.bridge.pid)})`) : n('no')}`,
    ].join('\n');
}
// ---------------------------------------------------------------------------
// (e) + (f) inline prompt helpers (each only one prompt; staying in this
// file keeps the call site readable)
// ---------------------------------------------------------------------------
async function runChannelOffer(secrets) {
    const channelChoice = await select({
        message: 'Set up Telegram channel now?',
        options: [
            { value: 'skip', label: "Skip for now (default) — I'll configure channels later" },
            { value: 'detect', label: 'Detect the existing bot token (no writes, no calls)' },
        ],
        initialValue: 'skip',
    });
    if (isCancel(channelChoice))
        return 'cancel';
    if (channelChoice === 'detect') {
        if (secrets.telegramTokenPresent) {
            note(`Token found at ${secrets.envPath}. Live delivery is offered after save (step f).`, 'Channel');
        }
        else {
            note('OPENSQUID_TELEGRAM_BOT_TOKEN not found. Add it to ~/.loop/.env before accepting the post-save test.', 'Channel');
        }
    }
    return 'ok';
}
export { runChatSetupWizard as runWizard };
//# sourceMappingURL=chat_actions.js.map