/**
 * Views API routes
 *
 * Per-user saved views backed by AuthService SQLite (Saved-Views Phase 2).
 * A view = a durable named lens (visualization + filters + sort/group, in
 * `config`) pinned to a `scope` (recent / type:<t> / folder:<id>). Views are
 * scoped to the authenticated user (or user_id=0 in no-auth mode).
 *
 * Scope resolution + access-control filtering live here (not in AuthService),
 * because they need the WorkspaceManager (filesystem) + access-control helpers:
 *   - `folder` save scopes mint/read the folder's stable sidecar id (D2).
 *   - on read, each stored scope is resolved to a concrete `ResolvedScope`
 *     and folder-scoped views the caller can no longer access are filtered out
 *     (§4a); a folder whose id no longer resolves is tombstoned as `gone`.
 */
import { hasRepoAccess, canWriteRepo } from '../../services/access-control.js';
import { reqGitUser } from './graph-api-access.js';
import { parseScope, buildTypeScope, buildFolderScope, isValidSaveScope, } from '../../core/view-scope.js';
export async function registerViewsApiRoutes(fastify, authService, workspaceManager) {
    function getUserId(req) {
        return req.user?.id ?? 0;
    }
    /**
     * Resolve a stored scope string to a `ResolvedScope` for the given caller.
     * A folder scope the caller can no longer reach — folder deleted (id resolves
     * to nothing) OR access revoked (`hasRepoAccess` false) — becomes `gone`, a
     * removable tombstone. We deliberately do NOT hide it: getUserViews returns
     * the caller's OWN views, so a hidden pin would be an invisible row the owner
     * can never delete. `gone` keeps it dimmed-but-removable in the sidebar.
     */
    function resolveScopeForCaller(scope, user, folderIndex) {
        const parsed = parseScope(scope);
        switch (parsed.kind) {
            case 'recent':
                return { kind: 'recent' };
            case 'type':
                return { kind: 'type', entityType: parsed.entityType };
            case 'invalid':
                return { kind: 'gone' };
            case 'folder': {
                const loc = folderIndex
                    ? (folderIndex.get(parsed.folderId) ?? null)
                    : workspaceManager.resolveFolderById(parsed.folderId);
                if (!loc)
                    return { kind: 'gone' };
                if (!hasRepoAccess(user, loc.repo, workspaceManager, authService))
                    return { kind: 'gone' };
                return { kind: 'folder', repo: loc.repo, folderPath: loc.folderPath };
            }
        }
    }
    function enrichViews(views, user) {
        // Resolve every folder scope against ONE folder-id index built in a single
        // pass over all repos, instead of one `resolveFolderById` per view (each of
        // which re-walks every repo's folder tree). Only built when a folder view is
        // present so the common all-recent case stays a pure SQL read.
        const folderIndex = views.some((v) => parseScope(v.scope).kind === 'folder')
            ? workspaceManager.buildFolderIdIndex()
            : undefined;
        return views.map((v) => ({ ...v, resolvedScope: resolveScopeForCaller(v.scope, user, folderIndex) }));
    }
    /**
     * Convert a client SaveScope into the stored scope string. Folder scopes
     * mint/read the folder's stable sidecar id so the view survives rename/move
     * (D2). Throws on an invalid scope (caller returns 400).
     */
    function storeScope(scope, req) {
        switch (scope.kind) {
            case 'recent':
                return 'recent';
            case 'type':
                return buildTypeScope(scope.entityType);
            case 'folder': {
                // ensureFolderId mints + git-commits a sidecar in the target repo, so it
                // is a write. Gate it on write access BEFORE touching the repo — without
                // this an authed non-member could force a sidecar commit in a folder
                // they can't access (read-time enrichment would later hide the view, but
                // the unauthorized write already happened).
                const user = req.user;
                if (!canWriteRepo(user, scope.repo, workspaceManager, authService)) {
                    throw new Error('No write access to that folder');
                }
                const id = workspaceManager.ensureFolderId(scope.repo, scope.folderPath, reqGitUser(req));
                return buildFolderScope(id);
            }
        }
    }
    // GET /api/views — list the caller's own (∪ adopted) views, scope-resolved
    // and access-filtered.
    fastify.get('/api/views', async (req, reply) => {
        const userId = getUserId(req);
        const user = req.user;
        return reply.send(enrichViews(authService.getUserViews(userId), user));
    });
    // POST /api/views — create a new view
    fastify.post('/api/views', async (req, reply) => {
        const userId = getUserId(req);
        const user = req.user;
        const { name, viewType, config, scope } = req.body ?? {};
        if (!name) {
            return reply.status(400).send({ error: 'name is required' });
        }
        const saveScope = scope ?? { kind: 'recent' };
        if (!isValidSaveScope(saveScope)) {
            return reply.status(400).send({ error: 'invalid scope' });
        }
        let storedScope;
        try {
            storedScope = storeScope(saveScope, req);
        }
        catch (err) {
            return reply.status(400).send({ error: err?.message ?? 'could not resolve scope' });
        }
        const view = authService.createUserView(userId, name, viewType ?? 'list', config ?? {}, user?.displayName ?? user?.email, storedScope);
        const resolvedScope = resolveScopeForCaller(view.scope, user);
        return reply.send({ ...view, resolvedScope });
    });
    // POST /api/views/reorder — persist the caller's pinned-view order. Static
    // path, so find-my-way matches it before `/api/views/:id`. Atomic in the DB
    // layer (one transaction) so a drag-reorder can't leave a scrambled order.
    fastify.post('/api/views/reorder', async (req, reply) => {
        const userId = getUserId(req);
        const ids = Array.isArray(req.body?.ids) ? req.body.ids.filter((n) => Number.isInteger(n)) : [];
        authService.reorderUserViews(userId, ids);
        return reply.send({ ok: true });
    });
    // POST /api/views/:id/share — toggle view visibility (inert until Phase 4)
    fastify.post('/api/views/:id/share', async (req, reply) => {
        const userId = getUserId(req);
        const viewId = Number(req.params.id);
        const existing = authService.getUserView(viewId, userId);
        if (!existing) {
            return reply.status(404).send({ error: 'View not found' });
        }
        const visibility = req.body?.visibility ?? 'private';
        authService.setViewVisibility(viewId, userId, visibility);
        return reply.send({ ok: true, visibility });
    });
    // PUT /api/views/:id — update a view (rename / re-scope / re-config / reorder)
    fastify.put('/api/views/:id', async (req, reply) => {
        const userId = getUserId(req);
        const viewId = Number(req.params.id);
        const existing = authService.getUserView(viewId, userId);
        if (!existing) {
            return reply.status(404).send({ error: 'View not found' });
        }
        let storedScope;
        if (req.body?.scope !== undefined) {
            if (!isValidSaveScope(req.body.scope)) {
                return reply.status(400).send({ error: 'invalid scope' });
            }
            try {
                storedScope = storeScope(req.body.scope, req);
            }
            catch (err) {
                return reply.status(400).send({ error: err?.message ?? 'could not resolve scope' });
            }
        }
        authService.updateUserView(viewId, userId, {
            name: req.body?.name,
            config: req.body?.config,
            viewType: req.body?.viewType,
            scope: storedScope,
            sortOrder: req.body?.sortOrder,
        });
        return reply.send({ ok: true });
    });
    // DELETE /api/views/:id — delete a view
    fastify.delete('/api/views/:id', async (req, reply) => {
        const userId = getUserId(req);
        const viewId = Number(req.params.id);
        const existing = authService.getUserView(viewId, userId);
        if (!existing) {
            return reply.status(404).send({ error: 'View not found' });
        }
        authService.deleteUserView(viewId, userId);
        return reply.send({ ok: true });
    });
    // NOTE: the workspace_views multi-view-checkoff API (/api/workspace-views)
    // was removed in Saved-Views Phase 1 — the single-current-view + scratch
    // model replaced it. The auth.db table is left inert (no destructive
    // migration on live workspaces).
}
//# sourceMappingURL=views-api.js.map