/**
 * Workspace API routes
 *
 * Git operations: status, commit, push, pull across all repos.
 */
import { existsSync, statSync } from 'fs';
import { join } from 'path';
import archiver from 'archiver';
import * as access from '../../services/access-control.js';
import { log } from '../../utils/log.js';
/** Repo names eligible for local git ops (status, commit, revert) — includes personal. */
function getLocalRepoNames(wm) {
    return wm.getAllRepoConfigs().map(r => r.name);
}
function getReadableLocalRepoNames(user, wm, authService) {
    return getLocalRepoNames(wm).filter(name => access.hasRepoAccess(user, name, wm, authService));
}
function getWritableLocalRepoNames(user, wm, authService) {
    return getLocalRepoNames(wm).filter(name => access.canWriteRepo(user, name, wm, authService));
}
function getWritableSyncRepoNames(user, wm, authService) {
    return wm
        .getAllRepoConfigs()
        .filter(repo => !repo.personal && access.canWriteRepo(user, repo.name, wm, authService))
        .map(repo => repo.name);
}
export async function registerWorkspaceApiRoutes(fastify, workspaceManager, gitUser, authService) {
    // GET /api/status — git status across all repos
    fastify.get('/api/status', async (req, reply) => {
        const user = req.user;
        const repoNames = getReadableLocalRepoNames(user, workspaceManager, authService);
        const results = [];
        for (const name of repoNames) {
            try {
                const graph = workspaceManager.getGraph(name);
                const git = graph.gitService;
                if (!git)
                    continue;
                // Build lookups from loaded entities
                const entities = graph.search({});
                const nameMap = new Map();
                const typeMap = new Map(); // entityType → entity names
                for (const e of entities) {
                    const eName = e.data?.name ?? e.id;
                    nameMap.set(e.id, eName);
                    const eType = e.entityType ?? '';
                    if (eType) {
                        const list = typeMap.get(eType) ?? [];
                        list.push(eName);
                        typeMap.set(eType, list);
                    }
                }
                const status = await git.status();
                if (status && status.length > 0) {
                    results.push({
                        repo: name,
                        changes: status.map((s) => {
                            const filePath = s.path ?? s;
                            const fileStatus = s.status ?? 'modified';
                            // Try direct entity ID match from path segments
                            const segments = filePath.replace(/\/$/, '').split('/').filter(Boolean);
                            for (const seg of segments) {
                                const clean = seg.replace(/\.md$/, '');
                                if (nameMap.has(clean)) {
                                    return { path: filePath, status: fileStatus, entityName: nameMap.get(clean) };
                                }
                            }
                            // Directory match: "projects/" → entity type "project", list entity names
                            if (filePath.endsWith('/') && segments.length === 1) {
                                const dirName = segments[0];
                                for (const [eType, names] of typeMap) {
                                    if (dirName === eType || dirName === eType + 's' || dirName === eType + 'es') {
                                        return { path: filePath, status: fileStatus, entityName: names.join(', ') };
                                    }
                                }
                            }
                            return { path: filePath, status: fileStatus };
                        }),
                    });
                }
            }
            catch {
                // Skip repos without git
            }
        }
        return reply.send(results);
    });
    // POST /api/commit — commit all repos
    fastify.post('/api/commit', async (req, reply) => {
        const { message } = req.body ?? {};
        const commitMessage = message || 'Update from web UI';
        try {
            const user = req.user;
            const repoNames = getWritableLocalRepoNames(user, workspaceManager, authService);
            const committed = [];
            for (const name of repoNames) {
                try {
                    const graph = workspaceManager.getGraph(name);
                    const git = graph.gitService;
                    if (!git)
                        continue;
                    const status = await git.status();
                    if (status && status.length > 0) {
                        await git.commitAll(commitMessage, gitUser);
                        committed.push(name);
                    }
                }
                catch {
                    // Skip individual repo failures
                }
            }
            return reply.send({ committed });
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
    // POST /api/push — push writable repos (skip personal)
    fastify.post('/api/push', async (req, reply) => {
        try {
            const user = req.user;
            const repoNames = getWritableSyncRepoNames(user, workspaceManager, authService);
            const pushed = [];
            for (const name of repoNames) {
                try {
                    const graph = workspaceManager.getGraph(name);
                    const git = graph.gitService;
                    if (!git)
                        continue;
                    await git.push();
                    pushed.push(name);
                }
                catch {
                    // Skip individual repo failures
                }
            }
            return reply.send({ pushed });
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
    // POST /api/pull — pull writable repos (skip personal)
    fastify.post('/api/pull', async (req, reply) => {
        try {
            const user = req.user;
            const repoNames = getWritableSyncRepoNames(user, workspaceManager, authService);
            const pulled = [];
            for (const name of repoNames) {
                try {
                    const graph = workspaceManager.getGraph(name);
                    if (!graph)
                        continue;
                    // pullAndReconcile runs the pull AND smart-merges the new disk
                    // content into any live editor Y.Doc the pull changed — without it,
                    // a user editing a pulled-updated entry overwrites the pulled
                    // changes on their next autosave (silent loss).
                    await graph.pullAndReconcile();
                    pulled.push(name);
                }
                catch {
                    // Skip individual repo failures
                }
            }
            return reply.send({ pulled });
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
    // POST /api/revert — revert all uncommitted changes across all repos
    fastify.post('/api/revert', async (req, reply) => {
        try {
            const user = req.user;
            const repoNames = getWritableLocalRepoNames(user, workspaceManager, authService);
            const reverted = [];
            for (const name of repoNames) {
                try {
                    const graph = workspaceManager.getGraph(name);
                    const git = graph.gitService;
                    if (!git)
                        continue;
                    const status = await git.status();
                    if (status && status.length > 0) {
                        git.revertAll();
                        reverted.push(name);
                    }
                }
                catch {
                    // Skip individual repo failures
                }
            }
            return reply.send({ reverted });
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
    // GET /api/repos/:repo/status — git status for a single repo
    fastify.get('/api/repos/:repo/status', async (req, reply) => {
        const user = req.user;
        if (!access.hasRepoAccess(user, req.params.repo, workspaceManager, authService)) {
            return reply.status(404).send({ error: 'Repo not found' });
        }
        try {
            const graph = workspaceManager.getGraph(req.params.repo);
            const git = graph.gitService;
            if (!git)
                return reply.status(404).send({ error: 'Repo not found' });
            const changes = await git.status();
            const dirty = changes ? changes.length : 0;
            let hasUpstream = false;
            let ahead = 0;
            let behind = 0;
            try {
                const ab = git.getAheadBehind();
                ahead = ab.ahead;
                behind = ab.behind;
                hasUpstream = true;
            }
            catch {
                // no upstream configured
            }
            return reply.send({ dirty, hasUpstream, ahead, behind, changes: changes || [] });
        }
        catch {
            return reply.status(404).send({ error: 'Repo not found' });
        }
    });
    // POST /api/repos/:repo/publish — commit + push a single repo
    fastify.post('/api/repos/:repo/publish', async (req, reply) => {
        const user = req.user;
        // Publish writes to the shared remote — require write access.
        if (!access.canWriteRepo(user, req.params.repo, workspaceManager, authService)) {
            return reply.status(404).send({ error: 'Repo not found' });
        }
        try {
            const graph = workspaceManager.getGraph(req.params.repo);
            const git = graph.gitService;
            if (!git)
                return reply.status(404).send({ error: 'Repo not found' });
            const status = await git.status();
            if (status && status.length > 0) {
                const message = req.body?.message || 'Update from editor';
                git.commitAll(message, gitUser);
            }
            try {
                git.push();
            }
            catch { /* no remote */ }
            return reply.send({ ok: true });
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
    // POST /api/repos/:repo/revert — revert uncommitted changes in a single repo
    fastify.post('/api/repos/:repo/revert', async (req, reply) => {
        const user = req.user;
        // Revert is destructive — require write access. A non-member must
        // not be able to discard another user's uncommitted work.
        if (!access.canWriteRepo(user, req.params.repo, workspaceManager, authService)) {
            return reply.status(404).send({ error: 'Repo not found' });
        }
        try {
            const graph = workspaceManager.getGraph(req.params.repo);
            const git = graph.gitService;
            if (!git)
                return reply.status(404).send({ error: 'Repo not found' });
            git.revertAll();
            return reply.send({ ok: true });
        }
        catch (err) {
            return reply.status(500).send({ error: err.message });
        }
    });
    // GET /api/stats — aggregate workspace stats (entities, links)
    fastify.get('/api/stats', async (_req, reply) => {
        const repos = workspaceManager.getAllRepoConfigs();
        let totalEntities = 0;
        let totalLinks = 0;
        for (const repoConfig of repos) {
            try {
                const graph = workspaceManager.getGraph(repoConfig.name);
                const entities = graph.search({});
                totalEntities += entities.length;
                for (const e of entities) {
                    totalLinks += (e.document?.wikilinks?.length ?? 0);
                }
            }
            catch {
                // Skip repos that fail
            }
        }
        return reply.send({
            entities: totalEntities,
            links: totalLinks,
        });
    });
    // GET /api/workspace/export — stream a zip of the caller's accessible
    // workspace (all shared folders they're a member of + their OWN personal
    // folder, plus a filtered .studiograph). This export is scoped to the
    // caller's own access, so it does NOT include other users' personal folders.
    // That's an access-scoping choice, not because personal folders are
    // "local-only" — they're committed like any folder. (The broader owner-level
    // `studiograph clone` with `include_private` DOES pull every personal folder.)
    //
    // The zip preserves each repo's .git directory so users can keep working
    // from the export (commit history intact). Excluded from .studiograph:
    // auth.db, server-auth.json, populate-cache/, memory/, *.log — same
    // exclusion list the CLI clone .gitignore enforces. Secrets and per-user
    // server state shouldn't leave the server in a portable archive.
    fastify.get('/api/workspace/export', async (req, reply) => {
        const user = req.user;
        const repos = user
            ? access.getAccessibleRepos(user, workspaceManager, authService)
            : workspaceManager.getAllRepoConfigs();
        const workspacePath = workspaceManager.getWorkspacePath();
        const dateStamp = new Date().toISOString().slice(0, 10);
        const slug = workspaceManager.config?.team_name
            ? String(workspaceManager.config.team_name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
            : 'studiograph';
        const filename = `${slug || 'studiograph'}-${dateStamp}.zip`;
        reply.header('Content-Type', 'application/zip');
        reply.header('Content-Disposition', `attachment; filename="${filename}"`);
        const archive = archiver('zip', { zlib: { level: 6 } });
        archive.on('warning', (err) => {
            // ENOENT on missing optional files is fine; bail on anything else.
            if (err?.code !== 'ENOENT')
                log.error('[export] archive warning:', err);
        });
        archive.on('error', (err) => {
            log.error('[export] archive error:', err);
            reply.raw.destroy(err);
        });
        // Pipe directly to the underlying socket — Fastify's reply.send() would
        // try to serialize the stream and add headers we've already set.
        archive.pipe(reply.raw);
        // Each accessible repo, with its .git history. Repos live at
        // `${workspacePath}/${repo.path}` per WorkspaceManager's layout.
        for (const repo of repos) {
            const repoPath = join(workspacePath, repo.path);
            if (!existsSync(repoPath))
                continue;
            try {
                if (!statSync(repoPath).isDirectory())
                    continue;
            }
            catch {
                continue;
            }
            archive.directory(repoPath, repo.path);
        }
        // .studiograph — workspace.json, sources, connectors, ABOUT.md, schema.
        // Mirrors the CLI clone .gitignore exclusion list so secrets, per-user
        // server state, caches, and logs stay on the server.
        const studiographPath = join(workspacePath, '.studiograph');
        if (existsSync(studiographPath)) {
            archive.glob('**/*', {
                cwd: studiographPath,
                ignore: [
                    'auth.db',
                    'auth.db-journal',
                    'auth-seed.json',
                    'server-auth.json',
                    // Public share-link bearer tokens — exporting these would hand every
                    // active anonymous link (and its entry mapping) to the recipient.
                    'share-links.db',
                    'share-links.db-wal',
                    'share-links.db-shm',
                    'populate-cache/**',
                    'memory/**',
                    'sessions/**',
                    '*.log',
                ],
                dot: true,
            }, { prefix: '.studiograph' });
        }
        await archive.finalize();
    });
}
//# sourceMappingURL=workspace-api.js.map