/**
 * studiograph pull / push commands
 *
 * Pull and push changes across the workspace:
 *   - the .studiograph config repo (if it has a git remote)
 *   - each entity folder that is a git repo with a remote
 *
 * Works with both GitHub remotes and the embedded git server.
 */
import { Command } from 'commander';
import { log, outro } from '@clack/prompts';
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { Workspace } from '../../core/workspace.js';
import { AuthService } from '../../services/auth-service.js';
import { ConnectorManager } from '../../mcp/connector-manager.js';
import { buildConfigBundle } from '../../core/secrets/bundle.js';
import { gitClone, gitPull, gitPush, gitPushWithCredentials, gitSetRemote, gitHasRemote } from '../../utils/git.js';
import { getStudiographToken } from '../../core/user-config.js';
// Owner-only admin escalation header for embedded-git operations. The server
// lifts the personal-folder restriction only when the caller is the workspace
// owner (access-control.ts: `includePrivate` is gated to owners and is a no-op
// for everyone else). `pull`/`push` are owner-CLI commands, so we send it
// unconditionally — non-owners get no extra access, and the owner can sync
// other users' private folders during migrations / offboarding. Without it the
// server returns 404 for any folder the caller doesn't personally own, even
// the workspace owner. `studiograph clone` sends the same header for the
// same reason, so clone/pull/push stay consistent.
const OWNER_INCLUDE_PRIVATE_HEADERS = ['X-Studiograph-Include-Private: 1'];
function resolveRemoteAuth(base, opts) {
    const token = opts.token?.trim() || process.env.STUDIOGRAPH_TOKEN?.trim() || getStudiographToken(base);
    if (!token) {
        throw new Error(`No API token for ${base}. Run \`studiograph admin token issue ${base}\` to mint one, or pass --token / STUDIOGRAPH_TOKEN.`);
    }
    return {
        // Both the main API middleware (server/index.ts) and the git-http handler
        // (routes/git-http.ts authenticateBasic) accept Bearer + Basic with an
        // sg_ token, so one token → one set of headers, no dual-format gymnastics.
        httpHeader: { Authorization: `Bearer ${token}` },
        gitCredentials: { username: '_token_', password: token },
    };
}
async function getRepoEntries(workspace, workspaceRoot) {
    const entries = [];
    // The .studiograph config directory is itself a git repo (when GitHub-provisioned)
    const configPath = join(workspaceRoot, '.studiograph');
    if (isGitRepo(configPath) && gitHasRemote(configPath)) {
        entries.push({ name: '.studiograph', path: configPath });
    }
    const config = await workspace.loadConfig();
    for (const repo of config.repos) {
        // Personal folders are excluded ONLY on this fallback path, which pushes
        // to whatever git remote was manually wired up — typically a third-party
        // host (GitHub, etc.) with no Studiograph permission layer to gate
        // access. Pushing a personal folder there would leak private content to
        // an ungated remote. The Studiograph-server paths (pull/pushRemoteData,
        // the server-aware push) DO sync personal folders, because git-http
        // gates them by owner.
        if (repo.personal)
            continue;
        const repoPath = join(workspaceRoot, repo.path);
        if (!isGitRepo(repoPath) || !gitHasRemote(repoPath))
            continue;
        entries.push({ name: repo.name, path: repoPath });
    }
    return entries;
}
function isGitRepo(path) {
    return existsSync(path) && existsSync(join(path, '.git'));
}
/**
 * Print manual resolution guidance when pull has diverged.
 */
function logManualGuidance(entryPath) {
    log.info(`     cd ${entryPath} && git pull --rebase`);
}
async function pullRemoteConfig(remoteUrl, includeData = false, authOpts = {}) {
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
        return;
    }
    const base = remoteUrl.replace(/\/+$/, '');
    const workspaceRoot = workspace.getWorkspacePath();
    const sgDir = join(workspaceRoot, '.studiograph');
    // Resolve authentication — sg_ token only (Phase C5).
    let auth;
    try {
        auth = resolveRemoteAuth(base, authOpts);
    }
    catch (err) {
        outro(err.message);
        process.exit(1);
        return;
    }
    // Fetch config bundle (fall back to auth seed for older servers)
    log.info('Fetching workspace config...');
    try {
        const bundleRes = await fetch(`${base}/api/config/bundle`, {
            headers: auth.httpHeader,
        });
        if (bundleRes.ok) {
            const bundle = await bundleRes.json();
            // 1. Auth seed — remote is authoritative for pull, so overwrite any
            // local-shadow rows (e.g. post-clone setup-wizard credentials that
            // would otherwise shadow the real production password_hash).
            writeFileSync(join(sgDir, 'auth-seed.json'), JSON.stringify(bundle.authSeed, null, 2), 'utf-8');
            // AuthService appends `.studiograph` to the path it's given —
            // pass workspace root, not `sgDir`, or we end up with
            // `.studiograph/.studiograph/auth.db` (disjoint from the running server).
            const authService = new AuthService(workspaceRoot);
            authService.applySeed({ mode: 'overwrite' });
            authService.close();
            log.success(`Team: ${bundle.authSeed.users.length} user${bundle.authSeed.users.length === 1 ? '' : 's'}`);
            // 2. Workspace config — merge: keep local-only fields (server_url), update shared fields.
            //
            // Phase 9b: route through `workspace.writeConfig()` so this CLI
            // pull path shares the same schema validator + dual-write seam as
            // every other writer.
            if (bundle.workspace) {
                const local = await workspace.loadConfig().catch(() => ({}));
                const merged = { ...local, ...bundle.workspace };
                if (local.server_url)
                    merged.server_url = local.server_url;
                workspace.writeConfig(merged);
                log.success(`Workspace config: ${bundle.workspace.repos?.length ?? 0} folder${(bundle.workspace.repos?.length ?? 0) === 1 ? '' : 's'}`);
            }
            // 3. ABOUT.md
            if (bundle.about !== null && bundle.about !== undefined) {
                writeFileSync(join(sgDir, 'ABOUT.md'), bundle.about, 'utf-8');
                log.success('Workspace identity (ABOUT.md)');
            }
            // 4. Sources
            const sourceNames = Object.keys(bundle.sources ?? {});
            if (sourceNames.length > 0) {
                const sourcesDir = join(sgDir, '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.success(`Sources: ${sourceNames.join(', ')}`);
            }
            // 5. Connectors (sanitized — no secrets).
            //
            // Phase 9b: route through `ConnectorManager.saveWorkspaceConfig` so
            // every connector save runs the same schema validator + stripSecrets
            // pass. The bundle is already sanitized server-side; the re-strip
            // is idempotent and preserves the single-seam invariant.
            if (bundle.connectors && bundle.connectors.length > 0) {
                for (const conn of bundle.connectors) {
                    ConnectorManager.saveWorkspaceConfig(conn, workspaceRoot);
                }
                log.success(`Connectors: ${bundle.connectors.map(c => c.name).join(', ')}`);
            }
            // Pull workspace data (markdown files) if requested
            if (includeData) {
                try {
                    await pullRemoteData(base, auth, workspaceRoot);
                }
                catch (err) {
                    log.error(`Data pull failed: ${err.message}`);
                }
            }
            outro(`Pulled from ${base}${includeData ? ' (config + data)' : ' (config)'}`);
        }
        else if (bundleRes.status === 404) {
            // Older server — fall back to auth seed only
            log.info('Server does not support config bundle, falling back to auth seed...');
            const seedRes = await fetch(`${base}/api/auth/seed`, { headers: auth.httpHeader });
            if (!seedRes.ok) {
                const body = await seedRes.json().catch(() => ({}));
                outro(`Failed to fetch seed: ${body.error || seedRes.statusText}`);
                process.exit(1);
                return;
            }
            const seed = await seedRes.json();
            writeFileSync(join(sgDir, 'auth-seed.json'), JSON.stringify(seed, null, 2), 'utf-8');
            // AuthService appends `.studiograph` — pass workspace root, not `sgDir`.
            const authService = new AuthService(workspaceRoot);
            authService.applySeed({ mode: 'overwrite' });
            authService.close();
            outro(`Pulled ${seed.users.length} user${seed.users.length === 1 ? '' : 's'} from ${base}`);
        }
        else {
            const body = await bundleRes.json().catch(() => ({}));
            outro(`Failed: ${body.error || bundleRes.statusText}`);
            process.exit(1);
        }
    }
    catch (err) {
        // Phase 9c: surface SchemaViolationError with its redaction-safe
        // message (field path + classification, no values). The error class
        // is detected by name to avoid pulling the schemas module into the
        // CLI bundle import graph just for an instanceof check.
        if (err?.name === 'SchemaViolationError' || err?.code === 'schema_violation') {
            outro(`Schema violation:\n${err.message}`);
            process.exit(1);
        }
        outro(`Failed: ${err.message}`);
        process.exit(1);
    }
}
// ─── Remote data pull (git-based) ───────────────────────────────────────────
// Clones or pulls each folder from the remote's embedded git HTTP server
// (/git/:repo). Uses git's native conflict detection — diverged folders
// are reported, not silently overwritten.
async function pullRemoteData(base, auth, workspaceRoot) {
    // Reload config — it was just updated by pullRemoteConfig
    const workspace = new Workspace(workspaceRoot);
    const config = await workspace.loadConfig();
    let cloned = 0, updated = 0, upToDate = 0, diverged = 0, errors = 0;
    for (const repo of config.repos) {
        const repoPath = join(workspaceRoot, repo.path);
        const remoteUrl = `${base}/git/${repo.name}`;
        // Clone if missing
        if (!existsSync(join(repoPath, '.git'))) {
            try {
                if (existsSync(repoPath))
                    rmSync(repoPath, { recursive: true, force: true });
                mkdirSync(repoPath, { recursive: true });
                gitClone(remoteUrl, repoPath, { silent: true, credentials: auth.gitCredentials, extraHeaders: OWNER_INCLUDE_PRIVATE_HEADERS });
                log.success(`${repo.name}: cloned`);
                cloned++;
            }
            catch (err) {
                log.error(`${repo.name}: clone failed — ${err.message}`);
                errors++;
            }
            continue;
        }
        // Ensure remote points at the requested server (may have been cloned elsewhere)
        try {
            gitSetRemote(repoPath, remoteUrl);
        }
        catch { /* non-fatal */ }
        try {
            const result = gitPull(repoPath, { credentials: auth.gitCredentials, extraHeaders: OWNER_INCLUDE_PRIVATE_HEADERS });
            if (result === 'updated') {
                log.success(`${repo.name}: updated`);
                updated++;
            }
            else if (result === 'up-to-date') {
                upToDate++;
            }
            else {
                log.warn(`${repo.name}: diverged — has local commits not on the server`);
                log.info(`     cd ${repoPath} && git pull --rebase`);
                diverged++;
            }
        }
        catch (err) {
            log.error(`${repo.name}: ${err.message}`);
            errors++;
        }
    }
    log.info(`Data: ${cloned} cloned, ${updated} updated, ${upToDate} up to date${diverged ? `, ${diverged} diverged` : ''}${errors ? `, ${errors} failed` : ''}`);
    if (diverged) {
        throw new Error('One or more folders diverged — resolve conflicts and retry');
    }
    if (errors) {
        throw new Error('One or more folders failed to pull');
    }
}
// ─── Remote data push (git-based) ───────────────────────────────────────────
// Pushes each local folder to the remote's embedded git HTTP server.
// Uses git's native conflict detection — rejected pushes require a pull first.
async function pushRemoteData(base, auth, workspace, workspaceRoot) {
    const config = await workspace.loadConfig();
    let pushed = 0, nothingToPush = 0, created = 0, rejected = 0, errors = 0;
    for (const repo of config.repos) {
        const repoPath = join(workspaceRoot, repo.path);
        if (!existsSync(join(repoPath, '.git'))) {
            log.warn(`${repo.name}: not a git repo — skipping`);
            continue;
        }
        try {
            // Ensure folder exists on remote (idempotent; 409 means it already exists).
            // Skip for personal folders: they're auto-provisioned server-side on login
            // (auth-api.ts#ensurePrivateFolder), and the POST /api/repos shared-folder
            // path rejects reserved `entries-<id>` names. Detect by flag OR name
            // pattern: clone now preserves `personal` (see clone.ts), so the flag is
            // authoritative for freshly-cloned workspaces — the name-pattern check is
            // a fallback for workspaces cloned before that fix landed. Mirrors the
            // server-aware push below.
            const skipServerFolderEnsure = !!repo.personal || /^entries-\d+$/.test(repo.name);
            if (!skipServerFolderEnsure) {
                const status = await ensureServerFolder(base, repo.name, !!repo.personal, auth.httpHeader);
                if (status === 'created') {
                    log.info(`${repo.name}: created on server`);
                    created++;
                }
            }
            // Point origin at the remote's git server
            gitSetRemote(repoPath, `${base}/git/${repo.name}`);
            // Push with credentials + owner escalation (so the owner can push to
            // any folder, including other users' private ones, during a migration).
            const result = gitPushWithCredentials(repoPath, auth.gitCredentials, { extraHeaders: OWNER_INCLUDE_PRIVATE_HEADERS });
            if (result === 'rejected') {
                log.warn(`${repo.name}: push rejected — run \`studiograph pull --server ${base} --data\` first`);
                rejected++;
                continue;
            }
            if (result === 'not-found') {
                log.warn(`${repo.name}: skipped — folder does not exist on the server (stale local clone; safe to remove locally)`);
                continue;
            }
            if (result === 'pushed') {
                log.success(`${repo.name}: pushed`);
                pushed++;
            }
            else {
                nothingToPush++;
            }
        }
        catch (err) {
            log.error(`${repo.name}: ${err.message}`);
            errors++;
        }
    }
    log.info(`Data: ${pushed} pushed${created ? `, ${created} created` : ''}, ${nothingToPush} unchanged${rejected ? `, ${rejected} rejected` : ''}${errors ? `, ${errors} failed` : ''}`);
    if (rejected) {
        throw new Error('One or more folders were rejected — pull and retry');
    }
    if (errors) {
        throw new Error('One or more folders failed to push');
    }
}
// ─── pull ─────────────────────────────────────────────────────────────────────
export const pullCommand = new Command('pull')
    .description('Pull latest changes from remote across the workspace')
    .option('--server <url>', 'Pull config from a remote Studiograph server')
    // `--remote` is a hidden alias kept for one release; remove in the next.
    // See cli-owner-audit §C5. Owner CLI is now the only caller, but scripts
    // and muscle memory may still reach for the old name.
    .option('--remote <url>', 'Deprecated alias for --server (removed in the next release)', undefined)
    .option('--data', 'Also pull workspace folder data (markdown files) from the remote server')
    .option('--token <token>', 'sg_ API token (also reads STUDIOGRAPH_TOKEN; mint via `studiograph admin token issue <url>`)')
    .action(async (opts) => {
    const flagServer = opts.server ?? opts.remote;
    if (opts.remote && !opts.server) {
        log.warn('`--remote` is deprecated, use `--server` instead.');
    }
    if (flagServer) {
        return pullRemoteConfig(flagServer, opts.data, { token: opts.token });
    }
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
        return;
    }
    const workspaceRoot = workspace.getWorkspacePath();
    let hasConflict = false;
    let hasError = false;
    // Preserve server_url before pulling — it's local-only and gets overwritten
    // when git pull brings down the config repo's workspace.json (which never has it).
    const prePullConfig = await workspace.loadConfig().catch(() => ({}));
    const savedServerUrl = prePullConfig.server_url;
    // ── Step 1: pull .studiograph first so workspace.json is up to date ──────
    const configPath = join(workspaceRoot, '.studiograph');
    if (isGitRepo(configPath) && gitHasRemote(configPath)) {
        try {
            const result = gitPull(configPath);
            if (result === 'updated') {
                log.success('.studiograph: updated');
            }
            else if (result === 'up-to-date') {
                log.info('.studiograph: already up to date');
            }
            else {
                // Diverged / any other non-clean state — surface it the same way
                // the folder loop does so the user actually sees the problem
                // instead of walking into a silent no-op.
                log.warn('.studiograph: diverged — has local commits not on the server');
                logManualGuidance(configPath);
                hasConflict = true;
            }
        }
        catch (error) {
            log.error(`.studiograph: ${error instanceof Error ? error.message : 'Unknown error'}`);
            hasError = true;
        }
    }
    // ── Step 2: reload config — picks up any new folders added server-side ─
    // Must use a fresh Workspace instance — loadConfig() caches this.config on
    // first call, so the original instance would return stale data after the pull.
    const freshWorkspace = new Workspace(workspaceRoot);
    const config = await freshWorkspace.loadConfig();
    // Re-apply server_url if the pull overwrote it (config repo never stores it).
    if (savedServerUrl && !config.server_url) {
        config.server_url = savedServerUrl;
        freshWorkspace.writeConfig(config);
    }
    const serverUrl = (config.server_url ?? savedServerUrl)?.replace(/\/+$/, '');
    // ── Step 3: pull or clone each folder ────────────────────────────────
    // Resolve git credentials for the embedded server:
    //   1. Stored JWT from `studiograph join` — per-user access control (preferred)
    //   2. Stored personal API token from ~/.studiograph/user.json
    let serverGitCredentials;
    if (serverUrl) {
        const serverAuthPath = join(workspaceRoot, '.studiograph', 'server-auth.json');
        if (existsSync(serverAuthPath)) {
            try {
                const serverAuth = JSON.parse(readFileSync(serverAuthPath, 'utf-8'));
                if (serverAuth.email && serverAuth.jwtToken) {
                    serverGitCredentials = { username: serverAuth.email, password: serverAuth.jwtToken };
                }
            }
            catch { /* non-fatal */ }
        }
        if (!serverGitCredentials) {
            const storedToken = getStudiographToken(serverUrl);
            if (storedToken) {
                serverGitCredentials = { username: '_token_', password: storedToken };
            }
        }
    }
    for (const repo of config.repos) {
        const repoPath = join(workspaceRoot, repo.path);
        // New folder — directory missing or exists but isn't a git repo (e.g. failed clone)
        if (!isGitRepo(repoPath)) {
            const cloneUrl = serverUrl ? `${serverUrl}/git/${repo.name}` : null;
            if (!cloneUrl) {
                log.warn(`${repo.name}: new folder has no remote — skipping`);
                continue;
            }
            try {
                const { gitClone } = await import('../../utils/git.js');
                const { rmSync } = await import('fs');
                // Remove any leftover empty/partial directory from a previous failed clone
                if (existsSync(repoPath))
                    rmSync(repoPath, { recursive: true, force: true });
                mkdirSync(repoPath, { recursive: true });
                gitClone(cloneUrl, repoPath, {
                    silent: true,
                    credentials: serverGitCredentials,
                    extraHeaders: OWNER_INCLUDE_PRIVATE_HEADERS,
                });
                log.success(`${repo.name}: cloned`);
            }
            catch (error) {
                log.error(`${repo.name}: clone failed — ${error instanceof Error ? error.message : 'Unknown error'}`);
                hasError = true;
            }
            continue;
        }
        // Existing folder — pull if it has a remote
        if (!isGitRepo(repoPath) || !gitHasRemote(repoPath))
            continue;
        try {
            const result = gitPull(repoPath, {
                credentials: serverGitCredentials,
                extraHeaders: OWNER_INCLUDE_PRIVATE_HEADERS,
            });
            if (result === 'updated') {
                log.success(`${repo.name}: updated`);
            }
            else if (result === 'up-to-date') {
                log.info(`${repo.name}: already up to date`);
            }
            else {
                log.warn(`${repo.name}: diverged — has local commits not on the server`);
                logManualGuidance(repoPath);
                hasConflict = true;
            }
        }
        catch (error) {
            log.error(`${repo.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
            hasError = true;
        }
    }
    // Apply auth seed if it was updated by pulling .studiograph.
    // Overwrite mode: the seed file just came from the remote config repo
    // and is authoritative over any local-shadow rows.
    const seedPath = join(workspaceRoot, '.studiograph', 'auth-seed.json');
    if (existsSync(seedPath)) {
        try {
            // AuthService appends `.studiograph` — pass workspace root.
            const authService = new AuthService(workspaceRoot);
            authService.applySeed({ mode: 'overwrite' });
            authService.close();
        }
        catch {
            // Non-fatal — seed apply can fail if DB is locked by running server
        }
    }
    if (hasConflict) {
        outro('⚠️  Pull complete with conflicts. Resolve them, then run `studiograph push`.');
        process.exit(1);
    }
    else if (hasError) {
        outro('❌ Pull failed for one or more folders.');
        process.exit(1);
    }
});
// ─── Remote config push ─────────────────────────────────────────────────────
async function pushRemoteConfig(remoteUrl, includeData = false, authOpts = {}) {
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
        return;
    }
    const base = remoteUrl.replace(/\/+$/, '');
    const workspaceRoot = workspace.getWorkspacePath();
    const sgDir = join(workspaceRoot, '.studiograph');
    // Resolve authentication — sg_ token only (Phase C5).
    let auth;
    try {
        auth = resolveRemoteAuth(base, authOpts);
    }
    catch (err) {
        outro(err.message);
        process.exit(1);
        return;
    }
    // Build config bundle from local workspace
    log.info('Building config bundle...');
    const config = await workspace.loadConfig();
    const about = workspace.loadAbout();
    // Auth seed
    let authSeed;
    const seedPath = join(sgDir, 'auth-seed.json');
    if (existsSync(seedPath)) {
        try {
            authSeed = JSON.parse(readFileSync(seedPath, 'utf-8'));
        }
        catch { /* skip */ }
    }
    // Sources
    const sources = {};
    const sourcesDir = join(sgDir, 'sources');
    if (existsSync(sourcesDir)) {
        for (const entry of readdirSync(sourcesDir)) {
            if (!entry.endsWith('.json'))
                continue;
            try {
                sources[entry.replace('.json', '')] = JSON.parse(readFileSync(join(sourcesDir, entry), 'utf-8'));
            }
            catch { /* skip */ }
        }
    }
    // Connectors (workspace-level, already sanitized)
    const connectors = ConnectorManager.loadWorkspaceConfigs(workspaceRoot);
    // Phase 3: route the bundle through the same sanitizer the server's
    // GET /api/config/bundle endpoint uses. CLI push and server export now
    // produce byte-identical output for the same workspace state — bootstrap
    // class fields (jwtSecret, deprecated apiKey) drop here so the POST
    // endpoint's strict-reject doesn't bounce older client builds.
    const bundle = buildConfigBundle({
        workspace: config,
        about,
        authSeed: (authSeed ?? { users: [] }),
        sources,
        connectors: connectors,
    });
    // Push to remote
    log.info('Pushing config...');
    try {
        const res = await fetch(`${base}/api/config/bundle`, {
            method: 'POST',
            headers: { ...auth.httpHeader, 'Content-Type': 'application/json' },
            body: JSON.stringify(bundle),
        });
        if (!res.ok) {
            const body = await res.json().catch(() => ({}));
            outro(`Failed: ${body.error || res.statusText}`);
            process.exit(1);
            return;
        }
        const result = await res.json();
        if (result.applied?.length) {
            for (const item of result.applied) {
                log.success(item);
            }
        }
        // Push workspace data (markdown files) if requested
        if (includeData) {
            try {
                await pushRemoteData(base, auth, workspace, workspaceRoot);
            }
            catch (err) {
                log.error(`Data push failed: ${err.message}`);
            }
        }
        outro(`Pushed to ${base}${includeData ? ' (config + data)' : ' (config)'}`);
    }
    catch (err) {
        outro(`Failed: ${err.message}`);
        process.exit(1);
    }
}
// ─── Server-aware push helpers ───────────────────────────────────────────────
function makeBasicAuth(username, password) {
    return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
}
async function serverFetch(serverUrl, path, authHeader) {
    const res = await fetch(`${serverUrl}${path}`, {
        headers: {
            'Authorization': authHeader,
            'Accept': 'application/json',
        },
    });
    if (!res.ok) {
        if (res.status === 401)
            throw new Error('Invalid credentials');
        const body = await res.json().catch(() => ({}));
        throw new Error(body.error || `Server returned ${res.status}`);
    }
    return res.json();
}
async function ensureServerFolder(serverUrl, repoName, isPersonal, authHeader) {
    const headers = { 'Content-Type': 'application/json' };
    if (typeof authHeader === 'string') {
        headers['Authorization'] = authHeader;
    }
    else {
        Object.assign(headers, authHeader);
    }
    const res = await fetch(`${serverUrl}/api/repos`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ name: repoName, ...(isPersonal ? { personal: true } : {}) }),
    });
    if (res.ok)
        return 'created';
    if (res.status === 409)
        return 'exists'; // already exists
    const body = await res.json().catch(() => ({}));
    throw new Error(body.error || `Failed to create folder "${repoName}": ${res.statusText}`);
}
// ─── push ─────────────────────────────────────────────────────────────────────
export const pushCommand = new Command('push')
    .description('Push local commits to remote across the workspace')
    .argument('[folder]', 'Push a specific folder (default: all)')
    .option('--server <url>', 'Push config to a remote Studiograph server')
    .option('--remote <url>', 'Deprecated alias for --server (removed in the next release)', undefined)
    .option('--data', 'Also push workspace folder data (markdown files) to the remote server')
    .option('--token <token>', 'sg_ API token (also reads STUDIOGRAPH_TOKEN; mint via `studiograph admin token issue <url>`)')
    .action(async (folder, opts) => {
    const flagServer = opts.server ?? opts.remote;
    if (opts.remote && !opts.server) {
        log.warn('`--remote` is deprecated, use `--server` instead.');
    }
    if (flagServer) {
        return pushRemoteConfig(flagServer, opts.data, { token: opts.token });
    }
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
        return;
    }
    const workspaceRoot = workspace.getWorkspacePath();
    const config = await workspace.loadConfig();
    const serverUrl = config.server_url?.replace(/\/+$/, '');
    // Push every folder — shared and personal alike. Personal folders are
    // per-user, but git-http (canAccessFolder → hasRepoAccess) gates by
    // owner so only the owner ever succeeds; the owner CLI sends the
    // include-private escalation, so the owner can push any of them.
    // Server-side, personal folders are auto-provisioned on login by
    // auth-api.ts#ensurePrivateFolder, so the per-folder ensureServerFolder()
    // call is skipped for them below (the createSharedFolder path would
    // reject `entries-<number>` as a reserved name even though the folder
    // already exists).
    let repos = config.repos;
    if (folder) {
        const match = repos.find(r => r.name === folder);
        if (!match) {
            log.error(`Folder "${folder}" not found in workspace.`);
            process.exit(1);
            return;
        }
        repos = [match];
    }
    // Server-aware push: workspace has a `server_url` (set by `studiograph clone`).
    if (serverUrl) {
        // sg_ token only. resolveRemoteAuth handles the "no token" error.
        let auth;
        try {
            auth = resolveRemoteAuth(serverUrl, { token: undefined });
        }
        catch (err) {
            outro(err.message);
            process.exit(1);
            return;
        }
        const authHeader = auth.httpHeader.Authorization;
        const gitCredentials = auth.gitCredentials;
        // Cheap probe — surfaces a 401/expired token before we touch git
        // (which would otherwise fail with a less useful "askpass auth
        // failed" message). One round-trip, idempotent.
        try {
            await serverFetch(serverUrl, '/api/workspace', authHeader);
        }
        catch (err) {
            outro(`Authentication failed (${err.message}). Run \`studiograph admin token issue ${serverUrl}\` to mint a fresh token.`);
            process.exit(1);
            return;
        }
        let hasError = false;
        for (const repo of repos) {
            const repoPath = join(workspaceRoot, repo.path);
            if (!isGitRepo(repoPath)) {
                log.warn(`${repo.name}: not a git repo, skipping`);
                continue;
            }
            // Personal folders skip the /api/repos POST. Detection has TWO
            // sources: the `personal` flag (clone now preserves it — see
            // clone.ts) and the reserved name pattern as a fallback for
            // workspaces cloned before that fix, whose workspace.json carries a
            // thin repo record without the flag. Anything matching
            // `entries-<digit>` is owned by the user with that id and was
            // auto-provisioned server-side via auth-api.ts#ensurePrivateFolder
            // on login. The /api/repos path goes through createSharedFolder
            // which rejects this exact pattern; the git push that follows
            // hits git-http directly where canAccessFolder gates by owner.
            const isPersonalByPattern = /^entries-\d+$/.test(repo.name);
            const skipServerFolderEnsure = !!repo.personal || isPersonalByPattern;
            try {
                if (!skipServerFolderEnsure) {
                    const status = await ensureServerFolder(serverUrl, repo.name, false, authHeader);
                    if (status === 'created') {
                        log.info(`${repo.name}: created on server`);
                    }
                }
                // Set origin to server git URL
                gitSetRemote(repoPath, `${serverUrl}/git/${repo.name}`);
                // Push with the resolved sg_ token plus the owner escalation header.
                // The server gates personal/solo folders to their owner-of-record —
                // even the workspace owner gets a 404 without this header (the
                // `includePrivate` flag is owner-only server-side, a no-op for
                // everyone else). Required so the owner can push other users' private
                // folders during migrations. Consistent with `studiograph clone`.
                let result = gitPushWithCredentials(repoPath, gitCredentials, { extraHeaders: OWNER_INCLUDE_PRIVATE_HEADERS });
                if (result === 'rejected') {
                    log.warn(`${repo.name}: push rejected — run \`studiograph pull\` first`);
                    hasError = true;
                    continue;
                }
                if (result === 'not-found') {
                    log.warn(`${repo.name}: skipped — folder does not exist on the server (stale local clone; safe to remove locally)`);
                    continue;
                }
                if (result === 'pushed') {
                    log.success(`${repo.name}: pushed`);
                }
                else {
                    log.info(`${repo.name}: nothing to push`);
                }
            }
            catch (error) {
                log.error(`${repo.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
                hasError = true;
            }
        }
        if (hasError) {
            outro('Push completed with errors.');
            process.exit(1);
        }
        return;
    }
    // Fallback: plain git push for repos that already have remotes
    const entries = await getRepoEntries(workspace, workspaceRoot);
    if (entries.length === 0) {
        log.info('No folders with remotes to push to. Use `studiograph clone <url>` to connect to a server.');
        return;
    }
    let hasError = false;
    for (const entry of entries) {
        if (folder && entry.name !== folder)
            continue;
        try {
            let result = gitPush(entry.path);
            if (result === 'rejected') {
                log.warn(`${entry.name}: push rejected — run \`studiograph pull\` first`);
                hasError = true;
                continue;
            }
            if (result === 'not-found') {
                log.warn(`${entry.name}: skipped — folder does not exist on the remote (stale local clone; safe to remove locally)`);
                continue;
            }
            if (result === 'pushed') {
                log.success(`${entry.name}: pushed`);
            }
            else {
                log.info(`${entry.name}: nothing to push`);
            }
        }
        catch (error) {
            log.error(`${entry.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
            hasError = true;
        }
    }
    if (hasError) {
        outro('Push failed for one or more folders.');
        process.exit(1);
    }
});
//# sourceMappingURL=sync.js.map