/**
 * Workspace management for Studiograph
 *
 * A workspace is a directory with .studiograph/ folder
 * Similar to how Git works with .git/
 */
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
import { execFileSync, spawnSync } from 'child_process';
import { join } from 'path';
import { setUserModelConfig } from './user-config.js';
import { enforceWorkspaceConfig } from './schemas/workspace.js';
import { sanitize } from '../server/middleware/sanitize.js';
export class Workspace {
    workspacePath;
    jsonConfigPath;
    membersPath;
    aboutPath;
    config = null;
    /** Suppress migration output (used during init where the caller owns the spinner) */
    quietMigrations = false;
    constructor(workspacePath = process.cwd()) {
        this.workspacePath = workspacePath;
        this.jsonConfigPath = join(workspacePath, '.studiograph', 'workspace.json');
        this.membersPath = join(workspacePath, '.studiograph', 'team', 'members.json');
        this.aboutPath = join(workspacePath, '.studiograph', 'ABOUT.md');
    }
    /**
     * Check if current directory is a Studiograph workspace
     */
    isWorkspace() {
        return existsSync(this.jsonConfigPath);
    }
    /**
     * Initialize new workspace in current directory
     */
    init(teamName) {
        if (this.isWorkspace()) {
            throw new Error('Already a Studiograph workspace');
        }
        // Create .studiograph directory and team subdirectory
        const studiographDir = join(this.workspacePath, '.studiograph');
        mkdirSync(join(studiographDir, 'team'), { recursive: true });
        // Create initial config
        const config = {
            version: '1.0.0',
            team_name: teamName,
            created_at: new Date().toISOString(),
            repos: [],
        };
        // Write config as workspace.json
        this.writeConfig(config);
        this.config = config;
        // Create initial empty team members file
        const initialMembers = { members: [] };
        writeFileSync(this.membersPath, JSON.stringify(initialMembers, null, 2), 'utf-8');
        // Create .gitignore for .studiograph
        const gitignorePath = join(studiographDir, '.gitignore');
        writeFileSync(gitignorePath, '# Ignore sensitive local config\n*.local\nauth.db\nauth.db-wal\nauth.db-shm\nauth-secret\napi-keys\n' +
            '# Yjs binary snapshot state (per-instance, not git-friendly)\nyjs-state/\n' +
            '# Running-server discovery file (per-instance, ephemeral)\nserver.json\n');
        // Create scaffolded ABOUT.md
        this.writeAbout('## Vision\n\n## Mission\n\n## Profile\n\n## Capabilities\n\n## Values\n');
    }
    /**
     * Load workspace config from workspace.json
     */
    async loadConfig() {
        if (!this.isWorkspace()) {
            throw new Error('Not a Studiograph workspace. Run `studiograph init` first.');
        }
        if (this.config) {
            return this.config;
        }
        const configJson = readFileSync(this.jsonConfigPath, 'utf-8');
        this.config = JSON.parse(configJson);
        return this.config;
    }
    /**
     * Write workspace config as workspace.json.
     *
     * Phase 5 dual-write: the R2 credential fields
     * (`r2_config.access_key_id`, `r2_config.secret_access_key`) and the
     * search api_key are mirrored into the encrypted store (when the
     * store has been initialized). Failures never block the file write.
     *
     * Phase 9c: every write goes through `enforceWorkspaceConfig`, which
     * throws `SchemaViolationError` when `STUDIOGRAPH_SCHEMA_STRICT` is on
     * (default) and the validator finds a secret-shaped field, or falls
     * back to warn-mode logging when the env is `'0'`. The throw blocks
     * the write — workspace.json is committed and must never carry
     * literals.
     */
    writeConfig(config) {
        // Phase 9c: strict-or-warn schema enforcement. In strict mode (the
        // default), violations throw and abort the write; callers translate
        // into HTTP 400 (config-api) or CLI exit 1 (sync/clone/join). In
        // warn-mode fallback (`STUDIOGRAPH_SCHEMA_STRICT=0`) the previous
        // log-only behaviour is preserved for hotfix.
        enforceWorkspaceConfig(config);
        writeFileSync(this.jsonConfigPath, JSON.stringify(config, null, 2), 'utf-8');
        void this._dualWrite(config);
    }
    /**
     * Lazy import of the dual-write helper. Best-effort — `mirrorWorkspaceConfig`
     * itself short-circuits when the store hasn't been initialized
     * (e.g. very early boot, tests without a store). Failures log + swallow.
     */
    async _dualWrite(config) {
        try {
            const { mirrorWorkspaceConfig } = await import('./secrets/dual-write.js');
            await mirrorWorkspaceConfig(config);
        }
        catch (err) {
            // eslint-disable-next-line no-console
            console.warn(`[secrets] dual-write workspace.json mirror failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    /**
     * Commit workspace.json and push to the config repo remote (if one exists).
     * Mirrors the same pattern used by AuthService.pushSeed() for auth-seed.json.
     * Non-fatal — logs warnings but never throws.
     */
    pushWorkspaceConfig(message) {
        const sgDir = join(this.workspacePath, '.studiograph');
        try {
            const isRepo = spawnSync('git', ['rev-parse', '--git-dir'], {
                cwd: sgDir, encoding: 'utf-8', stdio: 'pipe',
            });
            if (isRepo.status !== 0)
                return;
            const hasRemote = spawnSync('git', ['remote'], {
                cwd: sgDir, encoding: 'utf-8', stdio: 'pipe',
            });
            if (hasRemote.status !== 0 || !hasRemote.stdout.trim())
                return;
            spawnSync('git', ['add', 'workspace.json'], {
                cwd: sgDir, encoding: 'utf-8', stdio: 'pipe',
            });
            const diff = spawnSync('git', ['diff', '--cached', '--quiet'], {
                cwd: sgDir, encoding: 'utf-8', stdio: 'pipe',
            });
            if (diff.status === 0)
                return; // nothing staged
            const commit = spawnSync('git', [
                '-c', 'user.name=Studiograph',
                '-c', 'user.email=noreply@studiograph.local',
                'commit', '-m', message,
            ], { cwd: sgDir, encoding: 'utf-8', stdio: 'pipe' });
            if (commit.status !== 0) {
                // Phase 11: git stderr can echo remote URL credentials on
                // failure — route through sanitize() before logging.
                console.warn(`Workspace: failed to commit workspace.json — ${sanitize((commit.stderr || '').trim())}`);
                return;
            }
            const push = spawnSync('git', ['push'], {
                cwd: sgDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30_000,
            });
            if (push.status !== 0) {
                console.warn(`Workspace: failed to push workspace.json — ${sanitize((push.stderr || '').trim())}`);
            }
        }
        catch (err) {
            console.warn(`Workspace: config sync failed — ${sanitize(err.message)}`);
        }
    }
    /**
     * Load team members from team/members.json
     */
    loadMembers() {
        if (!existsSync(this.membersPath)) {
            return [];
        }
        const membersJson = readFileSync(this.membersPath, 'utf-8');
        const data = JSON.parse(membersJson);
        return data.members;
    }
    /**
     * Write team members to team/members.json
     */
    writeMembers(members) {
        const membersDir = join(this.workspacePath, '.studiograph', 'team');
        mkdirSync(membersDir, { recursive: true });
        writeFileSync(this.membersPath, JSON.stringify({ members }, null, 2), 'utf-8');
    }
    /**
     * Load ABOUT.md content. Returns null if file doesn't exist.
     */
    loadAbout() {
        if (!existsSync(this.aboutPath))
            return null;
        return readFileSync(this.aboutPath, 'utf-8');
    }
    /**
     * Write ABOUT.md content.
     */
    writeAbout(content) {
        writeFileSync(this.aboutPath, content, 'utf-8');
    }
    /**
     * Update model configuration.
     *
     * Provider and model ID are written to user config (~/.studiograph/user.json)
     * so they are personal and never committed to the shared workspace repo.
     * The API key is also written only to user config — it is never stored in
     * workspace.json.
     */
    setModelConfig(provider, modelId, apiKey) {
        setUserModelConfig(provider, modelId, apiKey);
    }
    /**
     * Register a new repo in the workspace
     */
    async registerRepo(repo) {
        const config = await this.loadConfig();
        // Check if repo already exists
        if (config.repos.find(r => r.name === repo.name)) {
            throw new Error(`Repo '${repo.name}' already registered`);
        }
        // Each user can have at most one personal folder
        if (repo.personal && repo.owner_id != null) {
            const existing = config.repos.find(r => r.personal === true && r.owner_id === repo.owner_id);
            if (existing) {
                throw new Error(`User ${repo.owner_id} already has a personal folder '${existing.name}'`);
            }
        }
        // Create repo config with path
        const repoConfig = {
            ...repo,
            path: repo.name, // Path relative to workspace root
            ...(repo.personal ? { personal: true } : {}),
        };
        // Add to config
        config.repos.push(repoConfig);
        this.writeConfig(config);
        this.config = config;
        this.pushWorkspaceConfig(`Add folder: ${repo.name}`);
        // Create repo directory
        const repoPath = join(this.workspacePath, repo.name);
        mkdirSync(repoPath, { recursive: true });
        if (repo.personal) {
            // Personal folders get no .studiograph marker (they aren't a team folder).
            // We deliberately do NOT add `<repo>/` to a workspace-root .gitignore:
            // every folder is its own git repo and nothing ever `git add`s from the
            // workspace root, so that entry was always inert — and misleading. Personal
            // folders ARE committed (to their own repo) and an admin/owner
            // `studiograph clone` pulls them all down. "Private" means in-app access
            // control + being skipped from the routine member push/pull, NOT "never
            // synced / never committed".
        }
        else {
            // Team folders: drop a .studiograph marker so other tools can detect
            // that the directory is a workspace repo. (No README — see commit
            // history; auto-generated READMEs added more cleanup churn than value.)
            const repoMarkerPath = join(repoPath, '.studiograph');
            writeFileSync(repoMarkerPath, JSON.stringify({ repo_name: repo.name }, null, 2), 'utf-8');
        }
        // Initialize git repo with .gitignore and initial commit
        const gitDir = join(repoPath, '.git');
        if (!existsSync(gitDir)) {
            const run = (args) => execFileSync('git', args, { cwd: repoPath, stdio: 'pipe' });
            run(['init']);
            run(['branch', '-M', 'main']);
            const gitignorePath = join(repoPath, '.gitignore');
            if (!existsSync(gitignorePath)) {
                writeFileSync(gitignorePath, '.DS_Store\n*.tmp\n*.log\n', 'utf-8');
            }
            run(['add', '-A']);
            run(['-c', 'user.name=Studiograph', '-c', 'user.email=noreply@studiograph.dev', 'commit', '-m', 'Initialize repo']);
        }
        return repoConfig;
    }
    /**
     * Get all registered repos
     */
    async getRepos() {
        const config = await this.loadConfig();
        return config.repos;
    }
    /**
     * Get repo by name
     */
    async getRepo(name) {
        const config = await this.loadConfig();
        return config.repos.find(r => r.name === name);
    }
    /**
     * Get absolute path to repo
     */
    async getRepoPath(repoName) {
        const repo = await this.getRepo(repoName);
        if (!repo) {
            throw new Error(`Repo '${repoName}' not found`);
        }
        return join(this.workspacePath, repo.path);
    }
    /**
     * Reconcile workspace.json with the filesystem.
     * Adds repos that exist on disk but aren't registered, removes entries
     * for repos whose directories are gone. Writes workspace.json only if
     * something changed.
     */
    async reconcileRepos() {
        const config = await this.loadConfig();
        const added = [];
        const removed = [];
        // ── Discover repos on disk ──────────────────────────────────────────
        const entries = readdirSync(this.workspacePath, { withFileTypes: true });
        for (const entry of entries) {
            if (!entry.isDirectory())
                continue;
            // Skip hidden dirs, .studiograph config dir, and personal folders
            if (entry.name.startsWith('.'))
                continue;
            const isPersonalDir = config.repos.some(r => r.path === entry.name && r.personal);
            if (isPersonalDir)
                continue;
            const dirPath = join(this.workspacePath, entry.name);
            const markerPath = join(dirPath, '.studiograph');
            // Must have a .studiograph marker *file* (not directory)
            if (!existsSync(markerPath))
                continue;
            try {
                const st = statSync(markerPath);
                if (!st.isFile())
                    continue;
            }
            catch {
                continue;
            }
            // Already registered?
            if (config.repos.find(r => r.name === entry.name))
                continue;
            // Read marker for name
            let repoName = entry.name;
            try {
                const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
                if (marker.repo_name)
                    repoName = marker.repo_name;
            }
            catch {
                // Marker unreadable — use defaults
            }
            const repoConfig = {
                name: repoName,
                path: entry.name,
            };
            config.repos.push(repoConfig);
            added.push(repoName);
        }
        // ── Remove stale entries ────────────────────────────────────────────
        config.repos = config.repos.filter(r => {
            const repoDir = join(this.workspacePath, r.path);
            if (existsSync(repoDir))
                return true;
            removed.push(r.name);
            return false;
        });
        // ── Persist if anything changed ─────────────────────────────────────
        if (added.length > 0 || removed.length > 0) {
            this.writeConfig(config);
            this.config = config;
        }
        return { added, removed };
    }
    /**
     * Get workspace root path
     */
    getWorkspacePath() {
        return this.workspacePath;
    }
    /**
     * Find workspace root by walking up directory tree
     * (like git does with .git/)
     */
    static findWorkspaceRoot(startPath = process.cwd()) {
        let currentPath = startPath;
        while (currentPath !== '/') {
            const jsonConfigPath = join(currentPath, '.studiograph', 'workspace.json');
            if (existsSync(jsonConfigPath)) {
                return currentPath;
            }
            // Go up one directory
            const parentPath = join(currentPath, '..');
            if (parentPath === currentPath)
                break; // Reached root
            currentPath = parentPath;
        }
        return null;
    }
}
//# sourceMappingURL=workspace.js.map