import { randomUUID } from 'node:crypto';
import { isAllowedOrigin } from '../config.js';
import { requireAllowedOrigin } from './authRoutes.js';
import { authenticateTerminalRequest } from './terminalRoutes.js';
import { proxyAgentRequest } from '../agent/agentProxy.js';
import { proxyAgentWebSocket } from '../agent/agentWebSocketProxy.js';
import { collectSystemStats } from '../system/stats.js';
import { createManualUpdateInfo, isPermissionUpdateFailure } from '../../shared/manualUpdate.js';
import { clearAgentManualUpdate, readAgentManualUpdate, rememberAgentManualUpdate } from '../update/manualUpdateState.js';
export async function registerAgentManagementRoutes(app, config, services) {
    // List agents
    app.get('/api/agents', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const state = await services.fileStore.read();
        return { agents: state.agents.map(({ id, name, url }) => ({ id, name, url })) };
    });
    // Read or update the main server's display name. The ServerList sidebar
    // shows this as the "Local" entry's label and lets the operator rename it
    // inline — without an endpoint the name was hardcoded to "Local". Stored
    // as null when the operator hasn't picked a name yet so the client knows
    // to fall back to its default label rather than rendering empty text.
    app.get('/api/main', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const state = await services.fileStore.read();
        return { name: state.mainName };
    });
    app.patch('/api/main', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const raw = request.body?.name;
        // Accept null/empty as "clear back to default" so the operator can
        // undo their rename without manual .env editing.
        const trimmed = typeof raw === 'string' ? raw.trim() : '';
        const next = trimmed.length > 0 ? trimmed.slice(0, 80) : null;
        await services.fileStore.update((state) => ({ ...state, mainName: next }));
        return { name: next };
    });
    // Add agent
    app.post('/api/agents', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const { name, url, token } = request.body ?? {};
        if (typeof name !== 'string' || !name.trim())
            return reply.code(400).send({ error: 'name_required' });
        if (typeof url !== 'string' || !url.trim())
            return reply.code(400).send({ error: 'url_required' });
        if (typeof token !== 'string' || token.length < 32)
            return reply.code(400).send({ error: 'token_too_short' });
        const normalizedUrl = normalizeAgentUrl(url);
        if (!normalizedUrl.ok) {
            return reply.code(400).send({ error: normalizedUrl.error });
        }
        const agentName = name.trim();
        let agent = null;
        let created = false;
        await services.fileStore.update((state) => {
            const tokenMatchIndex = state.agents.findIndex((a) => a.token === token);
            const urlMatchIndex = state.agents.findIndex((a) => a.url === normalizedUrl.url);
            const existingIndex = tokenMatchIndex >= 0 ? tokenMatchIndex : urlMatchIndex;
            if (existingIndex === -1) {
                created = true;
                agent = {
                    id: randomUUID(),
                    name: agentName,
                    url: normalizedUrl.url,
                    token,
                    createdAt: new Date().toISOString()
                };
                return { ...state, agents: [...state.agents, agent] };
            }
            const existing = state.agents[existingIndex];
            agent = {
                ...existing,
                name: agentName,
                url: normalizedUrl.url,
                token
            };
            const removedAgentIds = [];
            const agents = state.agents.flatMap((current, index) => {
                if (index === existingIndex) {
                    return [agent];
                }
                if (current.url === normalizedUrl.url || current.token === token) {
                    removedAgentIds.push(current.id);
                    return [];
                }
                return [current];
            });
            if (removedAgentIds.length === 0) {
                return { ...state, agents };
            }
            const agentLayouts = { ...state.agentLayouts };
            const agentManualUpdates = { ...state.agentManualUpdates };
            for (const removedAgentId of removedAgentIds) {
                const preferredLayout = pickPreferredAgentLayout(agentLayouts[existing.id], agentLayouts[removedAgentId]);
                if (preferredLayout) {
                    agentLayouts[existing.id] = preferredLayout;
                }
                else {
                    delete agentLayouts[existing.id];
                }
                const preferredManualUpdate = pickPreferredManualUpdate(agentManualUpdates[existing.id], agentManualUpdates[removedAgentId]);
                if (preferredManualUpdate) {
                    agentManualUpdates[existing.id] = preferredManualUpdate;
                }
                else {
                    delete agentManualUpdates[existing.id];
                }
                delete agentLayouts[removedAgentId];
                delete agentManualUpdates[removedAgentId];
            }
            return { ...state, agents, agentLayouts, agentManualUpdates };
        }, { flush: 'immediate' });
        return reply.code(created ? 201 : 200).send({ agent: { id: agent.id, name: agent.name, url: agent.url } });
    });
    app.put('/api/agents/order', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agentIds = Array.isArray(request.body?.agentIds) ? request.body.agentIds : null;
        if (!agentIds || agentIds.some((agentId) => typeof agentId !== 'string' || agentId.length === 0)) {
            return reply.code(400).send({ error: 'invalid_agent_order' });
        }
        let reorderedAgents = null;
        let validOrder = true;
        await services.fileStore.update((state) => {
            const currentIds = state.agents.map((agent) => agent.id);
            const uniqueIds = new Set(agentIds);
            if (agentIds.length !== currentIds.length ||
                uniqueIds.size !== agentIds.length ||
                currentIds.some((agentId) => !uniqueIds.has(agentId))) {
                validOrder = false;
                return state;
            }
            const agentById = new Map(state.agents.map((agent) => [agent.id, agent]));
            const agents = agentIds.map((agentId) => agentById.get(agentId));
            reorderedAgents = agents.map(({ id, name, url }) => ({ id, name, url }));
            return { ...state, agents };
        }, { flush: 'immediate' });
        if (!validOrder || !reorderedAgents) {
            return reply.code(400).send({ error: 'invalid_agent_order' });
        }
        return { agents: reorderedAgents };
    });
    // Rename agent
    app.patch('/api/agents/:agentId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const { name } = request.body ?? {};
        if (typeof name !== 'string' || !name.trim())
            return reply.code(400).send({ error: 'name_required' });
        let found = false;
        await services.fileStore.update((state) => {
            const agents = state.agents.map((a) => {
                if (a.id === request.params.agentId) {
                    found = true;
                    return { ...a, name: name.trim() };
                }
                return a;
            });
            return { ...state, agents };
        });
        if (!found)
            return reply.code(404).send({ error: 'agent_not_found' });
        return { ok: true };
    });
    // Remove agent
    app.delete('/api/agents/:agentId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const found = await removeAgentRegistration(services.fileStore, request.params.agentId);
        if (!found)
            return reply.code(404).send({ error: 'agent_not_found' });
        return reply.code(204).send();
    });
    app.post('/api/agents/:agentId/uninstall', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        const uninstall = await requestAgentUninstall(agent);
        const found = await removeAgentRegistration(services.fileStore, request.params.agentId);
        if (!found)
            return reply.code(404).send({ error: 'agent_not_found' });
        return {
            ok: true,
            message: uninstall.message,
            disconnecting: uninstall.disconnecting,
            disconnected: true
        };
    });
    // System stats for the main host. Sidebar polls this on a long interval
    // (~30s) so a delta-based CPU sample is meaningful without sampling
    // overhead — see src/server/system/stats.ts.
    app.get('/api/stats', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        return collectSystemStats(config.workspaceRoot);
    });
    // Proxy: stats for a registered agent
    app.get('/api/agents/:agentId/stats', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, '/stats', request, reply, {
            timeoutMs: 2500,
            transformJson: (json, status) => {
                if (status === 401 || status === 403) {
                    request.log.warn({ event: 'agent.stats_auth_failed', agentId: agent.id, status }, 'agent stats auth failed');
                    return { error: 'agent_unauthorized' };
                }
                return json;
            },
            mapStatus: (status) => (status === 401 || status === 403 ? 502 : status)
        });
    });
    // Ping agent
    app.get('/api/agents/:agentId/ping', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const state = await services.fileStore.read();
        const agent = state.agents.find((a) => a.id === request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        const start = Date.now();
        try {
            const res = await fetch(`${agent.url}/api/agent/terminals`, {
                headers: { Authorization: `Bearer ${agent.token}` },
                redirect: 'manual',
                signal: AbortSignal.timeout(5000)
            });
            const latencyMs = Date.now() - start;
            return { ok: res.ok, latencyMs };
        }
        catch {
            return { ok: false, latencyMs: Date.now() - start };
        }
    });
    // Proxy: version / update
    app.get('/api/agents/:agentId/version', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, '/version', request, reply, {
            transformJson: async (json) => {
                if (!isVersionInfoBody(json)) {
                    return json;
                }
                if (isManualUpdateInfo(json.manualUpdate)) {
                    await rememberAgentManualUpdate(services.fileStore, request.params.agentId, json.manualUpdate);
                    return json;
                }
                const remembered = await readAgentManualUpdate(services.fileStore, request.params.agentId);
                return remembered ? { ...json, manualUpdate: remembered } : json;
            }
        });
    });
    app.post('/api/agents/:agentId/update/check', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, '/update/check', request, reply, {
            transformJson: async (json) => {
                if (isUpdateCheckBody(json) && !json.error && json.updateAvailable === false) {
                    await clearAgentManualUpdate(services.fileStore, request.params.agentId);
                }
                return json;
            }
        });
    });
    app.post('/api/agents/:agentId/update', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, '/update', request, reply, {
            transformJson: async (json) => {
                if (!isUpdateResponseBody(json)) {
                    return json;
                }
                if (isManualUpdateInfo(json.manualUpdate)) {
                    await rememberAgentManualUpdate(services.fileStore, request.params.agentId, json.manualUpdate);
                    return json;
                }
                if (!json.ok && isPermissionUpdateFailure(json.message)) {
                    const manualUpdate = createManualUpdateInfo();
                    await rememberAgentManualUpdate(services.fileStore, request.params.agentId, manualUpdate);
                    return { ...json, message: manualUpdate.message, manualUpdate };
                }
                if (json.ok) {
                    await clearAgentManualUpdate(services.fileStore, request.params.agentId);
                }
                return json;
            }
        });
    });
    // Proxy: terminal list
    app.get('/api/agents/:agentId/terminals', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, '/terminals', request, reply);
    });
    app.get('/api/agents/:agentId/terminals/:terminalId/history', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
        return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/history${query}`, request, reply);
    });
    app.get('/api/agents/:agentId/terminals/:terminalId/transcript-history', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
        return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-history${query}`, request, reply, {
            unavailableFallback: {
                terminalId: request.params.terminalId,
                source: null,
                cursor: null,
                hasMore: false,
                output: []
            }
        });
    });
    app.get('/api/agents/:agentId/terminals/:terminalId/transcript-source', async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}/transcript-source`, request, reply, {
            unavailableFallback: { terminalId: request.params.terminalId, source: null }
        });
    });
    // Proxy: create terminal
    app.post('/api/agents/:agentId/terminals', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, '/terminals', request, reply);
    });
    // Proxy: close terminal
    app.delete('/api/agents/:agentId/terminals/:terminalId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
    });
    // Proxy: rename terminal
    app.patch('/api/agents/:agentId/terminals/:terminalId', { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
        const auth = await authenticateTerminalRequest(services.authService, request, reply);
        if (!auth)
            return reply;
        const agent = await resolveAgent(services.fileStore, request.params.agentId);
        if (!agent)
            return reply.code(404).send({ error: 'agent_not_found' });
        return proxyAgentRequest(agent, `/terminals/${request.params.terminalId}`, request, reply);
    });
    // Proxy: terminal WebSocket
    app.get('/api/agents/:agentId/terminals/:terminalId/ws', { websocket: true }, async (socket, request) => {
        if (!request.headers.origin || !isAllowedOrigin(config, request.headers.origin)) {
            socket.close(1008, 'origin not allowed');
            return;
        }
        const auth = await authenticateTerminalRequest(services.authService, request);
        if (!auth) {
            socket.close(1008, 'unauthorized');
            return;
        }
        const state = await services.fileStore.read();
        const agent = state.agents.find((a) => a.id === request.params.agentId);
        if (!agent) {
            socket.close(1008, 'agent not found');
            return;
        }
        proxyAgentWebSocket(socket, agent, request.params.terminalId);
    });
    // Proxy: file routes (all file operations)
    const filePathParts = ['root', 'list', 'read', 'write', 'create', 'move', 'delete-preview', 'delete', 'open', 'preview'];
    for (const part of filePathParts) {
        app.post(`/api/agents/:agentId/files/${part}`, { preHandler: (request, reply) => requireAllowedOrigin(config, request, reply) }, async (request, reply) => {
            const auth = await authenticateTerminalRequest(services.authService, request, reply);
            if (!auth)
                return reply;
            const agent = await resolveAgent(services.fileStore, request.params.agentId);
            if (!agent)
                return reply.code(404).send({ error: 'agent_not_found' });
            return proxyAgentRequest(agent, `/files/${part}`, request, reply);
        });
    }
}
async function resolveAgent(fileStore, agentId) {
    const state = await fileStore.read();
    return state.agents.find((a) => a.id === agentId) ?? null;
}
function pickPreferredAgentLayout(current, candidate) {
    if (!current)
        return candidate;
    if (!candidate)
        return current;
    if (candidate.revision !== current.revision) {
        return candidate.revision > current.revision ? candidate : current;
    }
    return timestampMs(candidate.updatedAt) > timestampMs(current.updatedAt) ? candidate : current;
}
function pickPreferredManualUpdate(current, candidate) {
    if (!current)
        return candidate;
    if (!candidate)
        return current;
    return timestampMs(candidate.detectedAt) > timestampMs(current.detectedAt) ? candidate : current;
}
function timestampMs(value) {
    const parsed = Date.parse(value);
    return Number.isFinite(parsed) ? parsed : 0;
}
function normalizeAgentUrl(rawUrl) {
    let parsedUrl;
    try {
        parsedUrl = new URL(rawUrl.trim());
    }
    catch {
        return { ok: false, error: 'invalid_url' };
    }
    if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
        return { ok: false, error: 'invalid_url_scheme' };
    }
    if (parsedUrl.username ||
        parsedUrl.password ||
        (parsedUrl.pathname && parsedUrl.pathname !== '/') ||
        parsedUrl.search ||
        parsedUrl.hash) {
        return { ok: false, error: 'invalid_url' };
    }
    return { ok: true, url: parsedUrl.origin };
}
function isVersionInfoBody(value) {
    return (value !== null &&
        typeof value === 'object' &&
        typeof value.version === 'string' &&
        typeof value.runningVersion === 'string');
}
function isManualUpdateInfo(value) {
    return (value !== null &&
        typeof value === 'object' &&
        value.reason === 'permission' &&
        typeof value.message === 'string' &&
        typeof value.command === 'string' &&
        typeof value.detectedAt === 'string');
}
function isUpdateCheckBody(value) {
    if (!value || typeof value !== 'object') {
        return false;
    }
    const candidate = value;
    return ((candidate.error === undefined || typeof candidate.error === 'string') &&
        (candidate.updateAvailable === undefined || candidate.updateAvailable === null || typeof candidate.updateAvailable === 'boolean'));
}
function isUpdateResponseBody(value) {
    return (value !== null &&
        typeof value === 'object' &&
        typeof value.ok === 'boolean' &&
        typeof value.changed === 'boolean' &&
        typeof value.message === 'string' &&
        typeof value.restarting === 'boolean');
}
async function removeAgentRegistration(fileStore, agentId) {
    let found = false;
    await fileStore.update((state) => {
        const agents = state.agents.filter((a) => {
            if (a.id === agentId) {
                found = true;
                return false;
            }
            return true;
        });
        // Drop the agent's stored layout too, so removed agents leave no orphaned state.
        const { [agentId]: _removed, ...agentLayouts } = state.agentLayouts;
        const { [agentId]: _removedManualUpdate, ...agentManualUpdates } = state.agentManualUpdates;
        return { ...state, agents, agentLayouts, agentManualUpdates };
    }, { flush: 'immediate' });
    return found;
}
async function requestAgentUninstall(agent) {
    try {
        const response = await fetch(`${agent.url}/api/agent/uninstall`, {
            method: 'POST',
            headers: { Authorization: `Bearer ${agent.token}` },
            redirect: 'manual',
            signal: AbortSignal.timeout(5000)
        });
        const body = await readOptionalJson(response);
        if (response.ok && isAgentUninstallBody(body) && body.ok) {
            return {
                message: body.message || 'Clean uninstall scheduled; disconnected from main.',
                disconnecting: body.disconnecting
            };
        }
        const detail = responseDetail(response, body);
        return {
            message: `Clean uninstall was not confirmed (${detail}); disconnected from main only.`,
            disconnecting: false
        };
    }
    catch {
        return {
            message: 'Agent unreachable; disconnected from main only.',
            disconnecting: false
        };
    }
}
async function readOptionalJson(response) {
    const contentType = response.headers.get('content-type') ?? '';
    if (!contentType.includes('application/json')) {
        return null;
    }
    try {
        return await response.json();
    }
    catch {
        return null;
    }
}
function isAgentUninstallBody(value) {
    return Boolean(value && typeof value === 'object' && 'ok' in value);
}
function responseDetail(response, body) {
    if (body && typeof body === 'object') {
        const message = body.error ?? body.message;
        if (typeof message === 'string' && message.trim()) {
            return message;
        }
    }
    return response.statusText || `HTTP ${response.status}`;
}
//# sourceMappingURL=agentManagementRoutes.js.map