/**
 * @fileoverview Shared helper functions for route modules.
 *
 * Contains pure functions extracted from server.ts and a session lookup helper
 * that replaces ~43 inline not-found checks across route handlers.
 */
import { join, resolve, relative, isAbsolute } from 'node:path';
import { realpathSync } from 'node:fs';
import fs from 'node:fs/promises';
import { homedir } from 'node:os';
import { ApiErrorCode, createErrorResponse } from '../types.js';
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
import { SseEvent } from './sse-events.js';
import { dataPath } from '../config/instance.js';
// Shared path constants used across route modules. CASES_DIR (project folders)
// stays shared across instances; SETTINGS_PATH is per-instance runtime state.
export const CASES_DIR = join(homedir(), 'codeman-cases');
export const SETTINGS_PATH = dataPath('settings.json');
/**
 * Validates that a path component doesn't escape the base directory.
 * Returns the resolved full path, or null if the path is a traversal attempt.
 */
export function validatePathWithinBase(name, baseDir) {
    const fullPath = resolve(join(baseDir, name));
    const resolvedBase = resolve(baseDir);
    const relPath = relative(resolvedBase, fullPath);
    if (relPath.startsWith('..') || isAbsolute(relPath)) {
        return null;
    }
    return fullPath;
}
/**
 * Reads and parses a JSON config file, returning a default value on ENOENT.
 * Logs an error for any I/O failure other than a missing file.
 */
export async function readJsonConfig(filePath, logLabel, defaultValue) {
    try {
        const content = await fs.readFile(filePath, 'utf-8');
        return JSON.parse(content);
    }
    catch (err) {
        if (err.code !== 'ENOENT') {
            console.error(`Failed to read ${logLabel}:`, err);
        }
        return defaultValue;
    }
}
/**
 * Validates that a file path (possibly containing symlinks) resolves to a location
 * within the given session working directory. Returns the resolved and relative paths,
 * or null if the path escapes the directory or doesn't exist.
 */
export function validateSessionFilePath(sessionWorkingDir, filePath) {
    const fullPath = resolve(sessionWorkingDir, filePath);
    let resolvedPath;
    try {
        resolvedPath = realpathSync(fullPath);
    }
    catch {
        return null;
    }
    const relativePath = relative(sessionWorkingDir, resolvedPath);
    if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
        return null;
    }
    return { resolvedPath, relativePath };
}
// Maximum hook data size (prevents oversized SSE broadcasts)
const MAX_HOOK_DATA_SIZE = 8 * 1024;
/**
 * Look up a session by ID or throw a structured error.
 * Replaces the pattern: `const session = sessions.get(id); if (!session) return createErrorResponse(...)`.
 */
export function findSessionOrFail(ctx, sessionId) {
    const session = ctx.sessions.get(sessionId);
    if (!session) {
        throw Object.assign(new Error(`Session ${sessionId} not found`), {
            statusCode: 404,
            body: createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${sessionId} not found`),
        });
    }
    return session;
}
/**
 * Parse and validate a request body against a Zod schema, or throw a structured 400 error.
 * Replaces the repeated pattern: `const r = Schema.safeParse(body); if (!r.success) return createErrorResponse(...)`.
 */
export function parseBody(schema, body, errorMessage) {
    const result = schema.safeParse(body);
    if (!result.success) {
        const msg = errorMessage ?? result.error.issues[0]?.message ?? 'Validation failed';
        throw Object.assign(new Error(msg), {
            statusCode: 400,
            body: createErrorResponse(ApiErrorCode.INVALID_INPUT, msg),
        });
    }
    return result.data;
}
/**
 * Persist session state and broadcast a SessionUpdated event.
 * Replaces the repeated two-line pattern across route handlers.
 */
export function persistAndBroadcastSession(ctx, session) {
    ctx.persistSessionState(session);
    ctx.broadcast(SseEvent.SessionUpdated, ctx.getSessionStateWithRespawn(session));
}
/**
 * Formats uptime in seconds to a human-readable string (e.g., "1d 2h 30m 15s").
 */
export function formatUptime(seconds) {
    const days = Math.floor(seconds / 86400);
    const hours = Math.floor((seconds % 86400) / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);
    const parts = [];
    if (days > 0)
        parts.push(`${days}d`);
    if (hours > 0)
        parts.push(`${hours}h`);
    if (minutes > 0)
        parts.push(`${minutes}m`);
    if (secs > 0 || parts.length === 0)
        parts.push(`${secs}s`);
    return parts.join(' ');
}
/**
 * Sanitizes hook event data before broadcasting via SSE.
 * Extracts only relevant fields and limits total size to prevent
 * oversized payloads from being broadcast to all connected clients.
 */
export function sanitizeHookData(data) {
    if (!data || typeof data !== 'object')
        return {};
    // Only forward known safe fields from Claude Code hook stdin
    const safeFields = {};
    const allowedKeys = [
        'hook_event_name',
        'tool_name',
        'tool_input',
        'session_id',
        'cwd',
        'permission_mode',
        'stop_hook_active',
        'transcript_path',
    ];
    for (const key of allowedKeys) {
        if (key in data && data[key] !== undefined) {
            safeFields[key] = data[key];
        }
    }
    // For tool_input, extract only summary fields (not full file content)
    if (safeFields.tool_input && typeof safeFields.tool_input === 'object') {
        const input = safeFields.tool_input;
        const summary = {};
        if (input.command)
            summary.command = String(input.command).slice(0, 500);
        if (input.file_path)
            summary.file_path = String(input.file_path).slice(0, 500);
        if (input.description)
            summary.description = String(input.description).slice(0, 200);
        if (input.query)
            summary.query = String(input.query).slice(0, 200);
        if (input.url)
            summary.url = String(input.url).slice(0, 500);
        if (input.pattern)
            summary.pattern = String(input.pattern).slice(0, 200);
        if (input.prompt)
            summary.prompt = String(input.prompt).slice(0, 200);
        safeFields.tool_input = summary;
    }
    // Final size check - drop if serialized data exceeds limit
    const serialized = JSON.stringify(safeFields);
    if (serialized.length > MAX_HOOK_DATA_SIZE) {
        return { tool_name: safeFields.tool_name, _truncated: true };
    }
    return safeFields;
}
/**
 * Toggles a service (watcher/manager) on or off based on an enabled flag.
 * Logs start/stop to console with the given label. Runs an optional callback after starting.
 */
export function toggleService(enabled, service, label, onStart) {
    if (enabled && !service.isRunning()) {
        service.start();
        onStart?.();
        console.log(`${label} started via settings change`);
    }
    else if (!enabled && service.isRunning()) {
        service.stop();
        console.log(`${label} stopped via settings change`);
    }
}
/**
 * Auto-configure Ralph tracker for a session.
 *
 * Priority order:
 * 1. .claude/ralph-loop.local.md (official Ralph Wiggum plugin state)
 * 2. CLAUDE.md <promise> tags (fallback)
 *
 * The ralph-loop.local.md file has priority because it contains
 * the exact configuration from an active Ralph loop session.
 */
export function autoConfigureRalph(session, workingDir, ctx) {
    // First, try to read the official Ralph Wiggum plugin state file
    const ralphConfig = parseRalphLoopConfig(workingDir);
    if (ralphConfig && ralphConfig.completionPromise) {
        session.ralphTracker.enable();
        session.ralphTracker.startLoop(ralphConfig.completionPromise, ralphConfig.maxIterations ?? undefined);
        // Restore iteration count if available
        if (ralphConfig.iteration > 0) {
            // The tracker's cycleCount will be updated when we detect iteration patterns
            // in the terminal output, but we can set maxIterations now
            console.log(`[auto-detect] Ralph loop at iteration ${ralphConfig.iteration}/${ralphConfig.maxIterations ?? '∞'}`);
        }
        console.log(`[auto-detect] Configured Ralph loop for session ${session.id} from ralph-loop.local.md: ${ralphConfig.completionPromise}`);
        ctx.broadcast(SseEvent.SessionRalphLoopUpdate, {
            sessionId: session.id,
            state: session.ralphTracker.loopState,
        });
        return;
    }
    // Fallback: try CLAUDE.md
    const claudeMdPath = join(workingDir, 'CLAUDE.md');
    const completionPhrase = extractCompletionPhrase(claudeMdPath);
    if (completionPhrase) {
        session.ralphTracker.enable();
        session.ralphTracker.startLoop(completionPhrase);
        console.log(`[auto-detect] Configured Ralph loop for session ${session.id} from CLAUDE.md: ${completionPhrase}`);
        ctx.broadcast(SseEvent.SessionRalphLoopUpdate, {
            sessionId: session.id,
            state: session.ralphTracker.loopState,
        });
    }
}
//# sourceMappingURL=route-helpers.js.map