import os from 'node:os';
import path from 'node:path';
import { promises as fsPromises } from 'node:fs';
import chokidar from 'chokidar';
import { sessionSynchronizerService } from '../../../modules/providers/services/session-synchronizer.service.js';
import { WS_OPEN_STATE, connectedClients } from '../../../modules/websocket/index.js';
import { getProjectsWithSessions } from '../../../modules/projects/index.js';
import { getDefaultOwnerUserId } from '../../../shared/default-user.js';
import { AppError } from '../../../shared/utils.js';
/**
 * Resolves the user id used to attribute background-watcher synchronizations.
 *
 * Returns `null` (with a console warning) when no user has registered yet so
 * that the watcher can keep running silently until the first account exists.
 */
function resolveWatcherOwnerUserId() {
    try {
        return getDefaultOwnerUserId();
    }
    catch (error) {
        if (error instanceof AppError && error.code === 'DEFAULT_USER_NOT_AVAILABLE') {
            console.warn('Session watcher skipping synchronization: no registered user yet');
            return null;
        }
        throw error;
    }
}
const PROVIDER_WATCH_PATHS = [
    {
        provider: 'claude',
        rootPath: path.join(os.homedir(), '.claude', 'projects'),
    },
    {
        provider: 'cursor',
        rootPath: path.join(os.homedir(), '.cursor', 'projects'),
    },
    {
        provider: 'codex',
        rootPath: path.join(os.homedir(), '.codex', 'sessions'),
    },
    // {
    //   provider: 'gemini',
    //   rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
    // },
    // Keep `sessions/` watcher disabled: Gemini also mirrors artifacts there,
    // which causes duplicate synchronization events.
    {
        provider: 'gemini',
        rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
    },
];
const WATCHER_IGNORED_PATTERNS = [
    '**/node_modules/**',
    '**/.git/**',
    '**/dist/**',
    '**/build/**',
    '**/*.tmp',
    '**/*.swp',
    '**/.DS_Store',
];
const PROJECTS_UPDATE_DEBOUNCE_MS = 500;
const PROJECTS_UPDATE_MAX_WAIT_MS = 2_000;
const watchers = [];
let pendingWatcherUpdate = null;
let pendingWatcherUpdateStartedAt = null;
let pendingWatcherFlushTimer = null;
let watcherRefreshInFlight = false;
let watcherRescheduleAfterRefresh = false;
/**
 * Filters watcher events to provider-specific session artifact file types.
 */
function isWatcherTargetFile(provider, filePath) {
    if (provider === 'gemini') {
        return filePath.endsWith('.json') || filePath.endsWith('.jsonl');
    }
    return filePath.endsWith('.jsonl');
}
function clearPendingWatcherFlushTimer() {
    if (pendingWatcherFlushTimer) {
        clearTimeout(pendingWatcherFlushTimer);
        pendingWatcherFlushTimer = null;
    }
}
function schedulePendingWatcherFlush() {
    if (!pendingWatcherUpdate) {
        return;
    }
    const now = Date.now();
    if (pendingWatcherUpdateStartedAt === null) {
        pendingWatcherUpdateStartedAt = now;
    }
    const elapsed = now - pendingWatcherUpdateStartedAt;
    const remainingMaxWait = Math.max(0, PROJECTS_UPDATE_MAX_WAIT_MS - elapsed);
    const delay = Math.min(PROJECTS_UPDATE_DEBOUNCE_MS, remainingMaxWait);
    clearPendingWatcherFlushTimer();
    pendingWatcherFlushTimer = setTimeout(() => {
        void flushPendingWatcherUpdate();
    }, delay);
}
function queuePendingWatcherUpdate(eventType, provider, updatedSessionId) {
    if (!pendingWatcherUpdate) {
        pendingWatcherUpdate = {
            providers: new Set(),
            changeTypes: new Set(),
            updatedSessionIds: new Set(),
        };
    }
    pendingWatcherUpdate.providers.add(provider);
    pendingWatcherUpdate.changeTypes.add(eventType);
    if (updatedSessionId) {
        pendingWatcherUpdate.updatedSessionIds.add(updatedSessionId);
    }
    schedulePendingWatcherFlush();
}
async function flushPendingWatcherUpdate() {
    clearPendingWatcherFlushTimer();
    if (!pendingWatcherUpdate) {
        return;
    }
    if (watcherRefreshInFlight) {
        watcherRescheduleAfterRefresh = true;
        return;
    }
    const queuedUpdate = pendingWatcherUpdate;
    pendingWatcherUpdate = null;
    pendingWatcherUpdateStartedAt = null;
    watcherRefreshInFlight = true;
    try {
        const ownerUserId = resolveWatcherOwnerUserId();
        const updatedProjects = ownerUserId !== null
            ? await getProjectsWithSessions(ownerUserId, { skipSynchronization: true })
            : [];
        const changeTypes = Array.from(queuedUpdate.changeTypes);
        const watchProviders = Array.from(queuedUpdate.providers);
        const updatedSessionIds = Array.from(queuedUpdate.updatedSessionIds);
        // Backward-compatible fields stay populated with the first queued values.
        const updateMessage = JSON.stringify({
            type: 'projects_updated',
            projects: updatedProjects,
            timestamp: new Date().toISOString(),
            changeType: changeTypes[0] ?? 'change',
            updatedSessionId: updatedSessionIds[0] ?? undefined,
            watchProvider: watchProviders[0] ?? undefined,
            changeTypes,
            updatedSessionIds,
            watchProviders,
            batched: true,
        });
        connectedClients.forEach(client => {
            if (client.readyState === WS_OPEN_STATE) {
                client.send(updateMessage);
            }
        });
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        console.error('Session watcher refresh failed while broadcasting projects_updated', { error: message });
    }
    finally {
        watcherRefreshInFlight = false;
        if (pendingWatcherUpdate || watcherRescheduleAfterRefresh) {
            watcherRescheduleAfterRefresh = false;
            schedulePendingWatcherFlush();
        }
    }
}
/**
 * Handles file watcher updates and triggers provider file-level synchronization.
 */
async function onUpdate(eventType, filePath, provider) {
    if (!isWatcherTargetFile(provider, filePath)) {
        return;
    }
    try {
        const ownerUserId = resolveWatcherOwnerUserId();
        if (ownerUserId === null) {
            return;
        }
        const result = await sessionSynchronizerService.synchronizeProviderFile(ownerUserId, provider, filePath);
        if (!result.indexed) {
            return;
        }
        console.log(`Session synchronization triggered by ${eventType} event for provider "${provider}"`, {
            filePath,
            sessionId: result.sessionId,
        });
        queuePendingWatcherUpdate(eventType, provider, result.sessionId);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        console.error(`Session watcher sync failed for provider "${provider}"`, {
            eventType,
            filePath,
            error: message,
        });
    }
}
/**
 * Starts provider filesystem watchers and performs initial DB synchronization.
 */
export async function initializeSessionsWatcher() {
    console.log('Setting up session watchers');
    const initialOwnerUserId = resolveWatcherOwnerUserId();
    if (initialOwnerUserId !== null) {
        const initialSync = await sessionSynchronizerService.synchronizeSessions(initialOwnerUserId);
        console.log('Initial session synchronization complete', {
            processedByProvider: initialSync.processedByProvider,
            failures: initialSync.failures,
        });
    }
    else {
        console.log('Initial session synchronization skipped: no registered user yet');
    }
    for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
        try {
            await fsPromises.mkdir(rootPath, { recursive: true });
            const watcher = chokidar.watch(rootPath, {
                ignored: WATCHER_IGNORED_PATTERNS,
                persistent: true,
                ignoreInitial: true,
                followSymlinks: false,
                depth: 6,
                usePolling: true,
                interval: 6_000,
                binaryInterval: 6_000,
            });
            watcher
                .on('add', (filePath) => {
                void onUpdate('add', filePath, provider);
            })
                .on('change', (filePath) => {
                void onUpdate('change', filePath, provider);
            })
                .on('error', (error) => {
                const message = error instanceof Error ? error.message : String(error);
                console.error(`Session watcher error for provider "${provider}"`, { error: message });
            });
            watchers.push(watcher);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            console.error(`Failed to initialize session watcher for provider "${provider}"`, {
                rootPath,
                error: message,
            });
        }
    }
}
/**
 * Stops all active provider session watchers.
 */
export async function closeSessionsWatcher() {
    clearPendingWatcherFlushTimer();
    await Promise.all(watchers.map(async (watcher) => {
        try {
            await watcher.close();
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            console.error('Failed to close session watcher', { error: message });
        }
    }));
    watchers.length = 0;
    pendingWatcherUpdate = null;
    pendingWatcherUpdateStartedAt = null;
    watcherRefreshInFlight = false;
    watcherRescheduleAfterRefresh = false;
}
//# sourceMappingURL=sessions-watcher.service.js.map