/**
 * studiograph clone command
 *
 * Clones an entire Studiograph workspace for owner setup on a new machine.
 * The CLI is an owner-only tool, so clone always pulls every folder —
 * shared and private alike (other members' personal folders included) —
 * and walks through full admin setup: LLM provider + connectors.
 *
 * Connects to a running Studiograph server using email/password and clones
 * all folders via the embedded git server.
 */
import { Command } from 'commander';
import { intro, confirm, outro, spinner, log, text, password as promptPassword, isCancel } from '@clack/prompts';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join, resolve } from 'path';
import { gitClone } from '../../utils/git.js';
import { runPreflight } from '../../utils/preflight.js';
import { reindexWorkspace } from '../../services/vector-service.js';
import { Workspace } from '../../core/workspace.js';
import { ConnectorManager } from '../../mcp/connector-manager.js';
import { AuthService } from '../../services/auth-service.js';
import { mintAndStoreCliToken } from '../../core/user-config.js';
import { promptLLMSetupIfNeeded, promptConnectorSetup, } from '../setup-wizard.js';
/**
 * Map a server repo record (`GET /api/repos`) into the per-repo entry written
 * to a cloned workspace.json. Preserves folder identity (display_name,
 * personal, owner_id) so the cloned workspace matches the server: without it
 * the UI title-cases the slug ("entries-1" → "Entries 1" instead of "My
 * Folder") and the sync paths fall back to guessing personal folders by name.
 *
 * Only fields the server actually returned are written — in particular a
 * shared folder is never stamped `personal: false`, which would mislead the
 * sync paths into treating it as a (skip-ensure) personal folder.
 */
export function buildClonedRepoRecord(r) {
    return {
        name: r.name,
        path: r.name,
        description: r.description,
        ...(r.display_name != null ? { display_name: r.display_name } : {}),
        ...(r.personal ? { personal: true } : {}),
        ...(r.owner_id != null ? { owner_id: r.owner_id } : {}),
    };
}
async function serverFetch(serverUrl, path, email, password, extraHeaders = {}) {
    const headers = {
        'Authorization': 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64'),
        'Accept': 'application/json',
        ...extraHeaders,
    };
    const res = await fetch(`${serverUrl}${path}`, { headers });
    if (!res.ok) {
        if (res.status === 401)
            throw new Error('Invalid credentials');
        throw new Error(`Server returned ${res.status}: ${res.statusText}`);
    }
    return res.json();
}
export const cloneCommand = new Command('clone')
    .description('Clone an entire workspace with full admin setup (LLM + connectors)')
    .argument('<server-url>', 'URL of the Studiograph server (e.g. https://studio.example.com)')
    .argument('[directory]', 'Directory to create workspace in (default: derived from workspace name, use "." for current directory)')
    .action(async (serverUrl, directory) => {
    intro('Clone Studiograph Workspace');
    const ready = runPreflight();
    if (!ready) {
        process.exit(1);
        return;
    }
    // Normalize URL — strip trailing slash
    serverUrl = serverUrl.replace(/\/+$/, '');
    try {
        // Step 1: Credentials. Prefer env vars when both are set (for
        // agent / CI use); fall back to interactive prompts otherwise.
        // Matches feedback_cli_non_interactive.md: every remote CLI
        // command should be runnable without a TTY.
        const envEmail = process.env.STUDIOGRAPH_EMAIL?.trim();
        const envPassword = process.env.STUDIOGRAPH_PASSWORD;
        let email;
        let pass;
        if (envEmail && envPassword) {
            if (!envEmail.includes('@')) {
                outro('STUDIOGRAPH_EMAIL is not a valid email address.');
                process.exit(1);
                return;
            }
            email = envEmail;
            pass = envPassword;
            log.info(`Using credentials from STUDIOGRAPH_EMAIL (${envEmail})`);
        }
        else {
            if (!process.stdout.isTTY) {
                outro('No TTY for interactive prompts. Set STUDIOGRAPH_EMAIL and STUDIOGRAPH_PASSWORD env vars to run non-interactively.');
                process.exit(1);
                return;
            }
            email = await text({ message: 'Email:', validate: (v) => v.includes('@') ? undefined : 'Enter a valid email' });
            if (isCancel(email)) {
                outro('Cancelled');
                process.exit(0);
                return;
            }
            pass = await promptPassword({ message: 'Password:' });
            if (isCancel(pass)) {
                outro('Cancelled');
                process.exit(0);
                return;
            }
        }
        // Step 2: Connect and fetch all repos
        const connectSpinner = spinner();
        connectSpinner.start('Connecting to server...');
        let workspaceName;
        let repos;
        // The CLI is an owner tool, so clone always pulls every folder —
        // shared and private alike (other members' personal folders
        // included). `include_private` is gated to owners server-side
        // (access-control.ts), so it's a safe no-op for any non-owner token.
        const reposPath = '/api/repos?include_private=1';
        try {
            const wsInfo = await serverFetch(serverUrl, '/api/workspace', email, pass);
            workspaceName = wsInfo.name;
            repos = await serverFetch(serverUrl, reposPath, email, pass);
            connectSpinner.stop(`Connected to ${workspaceName}`);
        }
        catch (err) {
            connectSpinner.stop('Connection failed');
            outro(`Could not connect: ${err instanceof Error ? err.message : 'Unknown error'}`);
            process.exit(1);
            return;
        }
        log.info(`${repos.length} folder${repos.length === 1 ? '' : 's'} in workspace`);
        if (repos.length === 0) {
            outro('No folders found.');
            process.exit(0);
            return;
        }
        // Step 3: Determine target directory
        let targetDir;
        if (directory === '.') {
            targetDir = process.cwd();
        }
        else if (directory) {
            targetDir = resolve(directory);
        }
        else {
            const slug = workspaceName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
            targetDir = resolve(slug);
        }
        const studiographDir = join(targetDir, '.studiograph');
        if (existsSync(studiographDir)) {
            outro(`A .studiograph directory already exists at ${targetDir}`);
            process.exit(1);
            return;
        }
        if (!existsSync(targetDir)) {
            mkdirSync(targetDir, { recursive: true });
            log.info(`Created directory: ${targetDir}`);
        }
        else if (directory !== '.') {
            const { readdirSync } = await import('fs');
            const files = readdirSync(targetDir);
            if (files.length > 0) {
                const proceed = await confirm({
                    message: `Directory ${targetDir} is not empty. Continue anyway?`,
                    initialValue: false,
                });
                if (!proceed || typeof proceed === 'symbol') {
                    outro('Cancelled');
                    process.exit(0);
                    return;
                }
            }
        }
        // Step 4: Write local workspace config.
        //
        // Phase 9b: route through `Workspace.writeConfig()` so this
        // freshly-built workspace.json is also validated by the schema
        // walker. The directory is created first (Workspace expects it).
        mkdirSync(studiographDir, { recursive: true });
        const workspaceConfig = {
            version: '1.0.0',
            team_name: workspaceName,
            created_at: new Date().toISOString(),
            server_url: serverUrl,
            repos: repos.map(buildClonedRepoRecord),
        };
        new Workspace(targetDir).writeConfig(workspaceConfig);
        // `server-auth.json` stays in the .gitignore list as a defensive measure
        // for users upgrading from older workspaces that still have the file on
        // disk (the file isn't written anymore as of A2 — see cli-owner-audit
        // §3.1 — but a leftover from a pre-upgrade clone shouldn't get checked in).
        writeFileSync(join(studiographDir, '.gitignore'), ['auth.db', 'auth-seed.json', 'server-auth.json', 'populate-cache/', 'memory/', '*.log'].join('\n') + '\n', 'utf-8');
        // Step 5: Clone all repos (one spinner reused across iterations; @clack's
        // spinner() registers process listeners per call and never removes them).
        let cloned = 0;
        let failed = 0;
        const repoSpinner = spinner();
        for (const repo of repos) {
            const repoPath = join(targetDir, repo.name);
            if (existsSync(repoPath)) {
                log.warn(`Skipping ${repo.name}: directory already exists`);
                continue;
            }
            repoSpinner.start(`Cloning ${repo.name}...`);
            try {
                const gitUrl = `${serverUrl}/git/${repo.name}`;
                gitClone(gitUrl, repoPath, {
                    credentials: { username: email, password: pass },
                    // Owner CLI always escalates — owner-gated server-side, no-op otherwise.
                    extraHeaders: ['X-Studiograph-Include-Private: 1'],
                });
                repoSpinner.stop(`Cloned ${repo.name}`);
                cloned++;
            }
            catch {
                repoSpinner.stop(`Failed to clone ${repo.name}`);
                log.warn(`Could not clone ${repo.name}: check your access permissions`);
                failed++;
            }
        }
        // Step 5b: Fetch config bundle (sources, connectors, ABOUT.md) — non-fatal
        try {
            const loginRes = await fetch(`${serverUrl}/api/auth/login`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ email, password: pass }),
            });
            if (loginRes.ok) {
                const cookies = loginRes.headers.getSetCookie?.() ?? [];
                const tokenCookie = cookies.find((c) => c.startsWith('__sg_token='));
                if (tokenCookie) {
                    const cookieHeader = tokenCookie.split(';')[0];
                    // Mint a personal sg_ API token using the just-issued JWT and
                    // persist it to ~/.studiograph/user.json. Subsequent
                    // pull/push/clear/etc. read from user.json — no more per-
                    // workspace JWT file. See cli-owner-audit §3.1 (A2): the JWT
                    // file write was the gating dependency for collapsing the
                    // CLI auth model to sg_-token-only. Non-fatal on failure —
                    // owner can re-mint via `studiograph admin token issue` (A4).
                    const mintResult = await mintAndStoreCliToken(serverUrl, cookieHeader, {
                        name: `clone-${new URL(serverUrl).hostname}-${new Date().toISOString().slice(0, 10)}`,
                    });
                    if (mintResult.ok) {
                        log.info(`API token saved to ~/.studiograph/user.json — future pull/push runs are non-interactive`);
                    }
                    else {
                        log.warn(`Could not mint CLI token (${mintResult.reason}). Run \`studiograph admin token issue ${serverUrl}\` later.`);
                    }
                    const bundleRes = await fetch(`${serverUrl}/api/config/bundle`, {
                        headers: { 'Cookie': cookieHeader },
                    });
                    if (bundleRes.ok) {
                        const bundle = await bundleRes.json();
                        if (bundle.authSeed) {
                            writeFileSync(join(studiographDir, 'auth-seed.json'), JSON.stringify(bundle.authSeed, null, 2), { encoding: 'utf-8', mode: 0o600 });
                            // AuthService constructor appends `.studiograph` to the path
                            // it's given. Passing `studiographDir` here would create
                            // `.studiograph/.studiograph/auth.db` — a duplicate-pathed DB
                            // that ends up disjoint from anything the server later opens
                            // via `targetDir`. Pass the workspace root.
                            const authService = new AuthService(targetDir);
                            try {
                                // overwrite mode: clone is a fresh checkout — there's no
                                // legitimate local state to preserve, and merge mode would
                                // shadow real credentials with any setup-wizard rows that
                                // got created before applySeed ran.
                                authService.applySeed({ mode: 'overwrite' });
                                const n = bundle.authSeed.users?.length ?? 0;
                                log.info(`Restored ${n} user${n === 1 ? '' : 's'} from workspace`);
                            }
                            finally {
                                authService.close();
                            }
                        }
                        if (bundle.sources && Object.keys(bundle.sources).length > 0) {
                            const sourcesDir = join(studiographDir, 'sources');
                            mkdirSync(sourcesDir, { recursive: true });
                            for (const [name, def] of Object.entries(bundle.sources)) {
                                writeFileSync(join(sourcesDir, `${name}.json`), JSON.stringify(def, null, 2), 'utf-8');
                            }
                            log.info(`Synced ${Object.keys(bundle.sources).length} source definition(s)`);
                        }
                        if (bundle.connectors?.length > 0) {
                            // Phase 9b: route through ConnectorManager.saveWorkspaceConfig
                            // so the schema validator runs uniformly.
                            for (const conn of bundle.connectors) {
                                ConnectorManager.saveWorkspaceConfig(conn, targetDir);
                            }
                            log.info(`Synced ${bundle.connectors.length} connector config(s)`);
                        }
                        if (bundle.about) {
                            writeFileSync(join(studiographDir, 'ABOUT.md'), bundle.about, 'utf-8');
                        }
                    }
                }
            }
        }
        catch (err) {
            // Phase 9c: a schema violation means the server shipped a literal
            // secret in the bundle — that's a server-side defect we should
            // surface, not silently swallow. Everything else stays non-fatal.
            if (err?.name === 'SchemaViolationError' || err?.code === 'schema_violation') {
                log.warn(`Connector config validation failed (skipped): ${err.message}`);
            }
            // Other errors are non-fatal (network blips, auth, missing bundle endpoint).
        }
        // Step 6: Index
        if (cloned > 0) {
            const indexSpinner = spinner();
            indexSpinner.start('Building search index...');
            try {
                const indexedRepos = repos.filter(r => existsSync(join(targetDir, r.name)));
                const total = await reindexWorkspace(targetDir, indexedRepos);
                indexSpinner.stop(`Search index ready (${total} entities)`);
            }
            catch {
                indexSpinner.stop('Search index skipped (non-fatal)');
            }
        }
        const parts = [];
        if (cloned > 0)
            parts.push(`${cloned} cloned`);
        if (failed > 0)
            parts.push(`${failed} failed`);
        // Step 7: LLM + connector setup
        await promptLLMSetupIfNeeded();
        await promptConnectorSetup(targetDir);
        const nextStep = directory === '.' ? '' : `\n\nNext: cd ${targetDir} && studiograph serve, then open the web UI`;
        if (failed > 0) {
            outro(`Cloned ${workspaceName} with issues (${parts.join(', ')}). Check access permissions.${nextStep}`);
        }
        else {
            outro(`Cloned ${workspaceName} workspace (${parts.join(', ')})${nextStep}`);
        }
    }
    catch (error) {
        outro(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
        process.exit(1);
    }
});
//# sourceMappingURL=clone.js.map