/**
 * Conversation session service.
 *
 * Boundary rule:
 * - hosts should call this service (or a later richer session service), not
 *   file-backed storage helpers directly;
 * - storage/repository code stays behind the service boundary;
 * - session semantics should grow here rather than leaking back into TUI/web
 *   host code.
 *
 * Current compromise:
 * this service currently uses the file-backed repository directly. That is the
 * intended direction for session persistence ownership, but some older host
 * flows still bypass the service and call the repository themselves. Those
 * flows should move inward to this service over time.
 */
import { randomUUID } from 'node:crypto';
import { join, resolve } from 'node:path';
import dayjs from 'dayjs';
import { FileChatSessionRepository } from '../../../../core/chat/engine/sessions/repository/index.js';
import { ChatSessionLeases } from '../../../../core/chat/engine/sessions/leases/index.js';
import { ConversationCompactionService } from '../../../../core/chat/engine/compaction/index.js';
import { ChatSessionRecords, ChatSessionTitles, ConversationLines } from '../../../../core/chat/engine/sessions/records/index.js';
export class FileConversationSessionService {
    repository;
    config;
    constructor(config) {
        this.config = FileConversationSessionService.normalizeConfig(config);
        this.repository = new FileChatSessionRepository({
            sessionStoragePath: this.config.sessionStoragePath,
        });
    }
    static summarize(session) {
        return ChatSessionRecords.summarize(session);
    }
    list() {
        return FileConversationSessionService.activeSessions(this.loadSessions());
    }
    listExisting() {
        return FileConversationSessionService.activeSessions(this.loadExistingSessions());
    }
    read(id) {
        return this.readSession(id);
    }
    require(id) {
        const session = this.readSession(id);
        if (!session) {
            throw new Error(`Chat session not found: ${id}`);
        }
        return session;
    }
    latest() {
        return this.list()[0];
    }
    latestExisting() {
        return this.listExisting()[0];
    }
    create(input) {
        const existing = this.loadExistingSessions();
        const session = ChatSessionRecords.create({
            id: input?.id?.trim() || `session-${randomUUID()}`,
            name: input?.name?.trim() || `Session ${FileConversationSessionService.getNextSessionNumber(existing)}`,
            model: input?.model ?? this.config.model,
            reasoningEffort: input?.reasoningEffort ?? this.config.reasoningEffort,
            workspaceId: input?.workspaceId ?? this.config.workspaceId,
            retention: input?.retention,
        });
        this.repository.save([session, ...existing]);
        return session;
    }
    createOneOff(input) {
        return this.create({
            ...input,
            name: input?.name?.trim() || `Ask ${new Date().toISOString()}`,
            retention: 'one_off',
        });
    }
    update(id, updater) {
        const sessions = this.loadSessions();
        const session = sessions.find((candidate) => candidate.id === id);
        if (!session) {
            return undefined;
        }
        const nextSession = updater(session);
        if (nextSession === session) {
            return session;
        }
        const touched = ChatSessionRecords.touch(nextSession);
        this.repository.save(sessions.map((candidate) => (candidate.id === id ? touched : candidate)));
        return touched;
    }
    updateSettings(id, input) {
        return this.updateRequiredSession(id, (session) => FileConversationSessionService.applySettings(session, input));
    }
    appendMessage(id, input) {
        return this.appendMessages(id, [input]);
    }
    appendMessages(id, inputs) {
        if (inputs.length === 0) {
            return this.require(id);
        }
        return this.updateRequiredSession(id, (session) => ({
            ...session,
            messages: [...session.messages, ...inputs],
        }));
    }
    markAcceptedUserMessage(id, input) {
        return this.updateRequiredSession(id, (session) => (ChatSessionRecords.markAcceptedUserMessage(session, input)));
    }
    acceptUserMessage(id, input) {
        return this.updateRequiredSession(id, (session) => {
            const conflict = ChatSessionLeases.conflict(session, input.leaseOwner);
            if (conflict) {
                throw new Error(conflict);
            }
            return ChatSessionRecords.markAcceptedUserMessage(ChatSessionLeases.acquire(session, input.leaseOwner), input);
        });
    }
    markAcceptedUserMessageFailed(id, input) {
        return this.updateRequiredSession(id, (session) => (ChatSessionRecords.markAcceptedUserMessageFailed(session, input)));
    }
    enqueuePrompt(id, input) {
        const prompt = input.prompt.trim();
        if (!prompt) {
            throw new Error('Queued prompt cannot be empty.');
        }
        const now = new Date().toISOString();
        const item = {
            id: `queued-prompt-${randomUUID()}`,
            prompt,
            createdAt: now,
            updatedAt: now,
        };
        const session = this.updateRequiredSession(id, (current) => ({
            ...current,
            queuedPrompts: [...current.queuedPrompts, item],
        }));
        return {
            session,
            item,
            position: session.queuedPrompts.findIndex((candidate) => candidate.id === item.id) + 1,
        };
    }
    updateQueuedPrompt(id, input) {
        const prompt = input.prompt.trim();
        if (!prompt) {
            throw new Error('Queued prompt cannot be empty.');
        }
        return this.updateRequiredSession(id, (session) => {
            if (!session.queuedPrompts.some((item) => item.id === input.queueItemId)) {
                throw new Error(`Queued prompt not found: ${input.queueItemId}`);
            }
            return {
                ...session,
                queuedPrompts: session.queuedPrompts.map((item) => (item.id === input.queueItemId
                    ? { ...item, prompt, updatedAt: new Date().toISOString() }
                    : item)),
            };
        });
    }
    deleteQueuedPrompt(id, input) {
        return this.updateRequiredSession(id, (session) => {
            if (!session.queuedPrompts.some((item) => item.id === input.queueItemId)) {
                return session;
            }
            return {
                ...session,
                queuedPrompts: session.queuedPrompts.filter((item) => item.id !== input.queueItemId),
            };
        });
    }
    dequeueQueuedPrompt(id) {
        let item;
        const session = this.updateRequiredSession(id, (current) => {
            if (current.queuedPrompts.length === 0) {
                return current;
            }
            item = current.queuedPrompts[0];
            return {
                ...current,
                queuedPrompts: current.queuedPrompts.slice(1),
            };
        });
        return {
            session,
            item,
        };
    }
    resetConversation(id) {
        return this.updateRequiredSession(id, (session) => ({
            ...session,
            history: [],
            turns: [],
            lastContinuePrompt: undefined,
            messages: [],
            queuedPrompts: [],
        }));
    }
    setLastContinuePrompt(id, prompt) {
        return this.updateRequiredSession(id, (session) => (session.lastContinuePrompt === prompt ? session : {
            ...session,
            lastContinuePrompt: prompt,
        }));
    }
    markCompactionRunning(id, input) {
        return this.updateRequiredSession(id, (session) => ({
            ...session,
            history: input.sourceHistory,
            context: ConversationCompactionService.buildSessionRunningContext({
                session,
                history: input.sourceHistory,
                lastArchivePath: input.archivePath,
            }),
        }));
    }
    applyCompactionResult(id, input) {
        return this.updateRequiredSession(id, (session) => ({
            ...session,
            history: input.history,
            context: input.context,
            archives: input.archive.archives,
            messages: ConversationLines.fromHistory(input.history),
        }));
    }
    restoreCompactionState(id, input) {
        return this.updateRequiredSession(id, (session) => ({
            ...session,
            ...input,
        }));
    }
    setDriftEnabled(id, enabled) {
        return this.updateRequiredSession(id, (session) => FileConversationSessionService.applySettings(session, { driftEnabled: enabled }));
    }
    getLeaseConflict(id, owner) {
        return ChatSessionLeases.conflict(this.require(id), owner);
    }
    acquireLease(id, owner) {
        return this.updateRequiredSession(id, (session) => {
            const conflict = ChatSessionLeases.conflict(session, owner);
            if (conflict) {
                throw new Error(conflict);
            }
            return ChatSessionLeases.acquire(session, owner);
        });
    }
    refreshLease(id, owner) {
        return this.updateRequiredSession(id, (session) => ChatSessionLeases.refresh(session, owner));
    }
    releaseLease(id, owner) {
        return this.updateRequiredSession(id, (session) => ChatSessionLeases.release(session, owner));
    }
    rename(id, name) {
        return this.updateRequiredSession(id, (session) => ({
            ...session,
            name,
        }));
    }
    setPinned(id, pinned) {
        return this.updateRequiredSession(id, (session) => (session.pinned === pinned ? session : {
            ...session,
            pinned,
        }));
    }
    setArchived(id, archived) {
        return this.updateRequiredSession(id, (session) => {
            if (archived) {
                return session.archivedAt ? session : {
                    ...session,
                    archivedAt: dayjs().toISOString(),
                };
            }
            return session.archivedAt === undefined ? session : {
                ...session,
                archivedAt: undefined,
            };
        });
    }
    async autoRenameAfterFirstUserMessage(id, input) {
        const session = this.read(id);
        if (!session || !ChatSessionRecords.canAutoRenameAfterFirstUserMessage(session)) {
            return { renamed: false, session };
        }
        const title = await ChatSessionTitles.generate(input);
        if (!title) {
            return { renamed: false, session };
        }
        const latest = this.require(id);
        if (!ChatSessionRecords.canAutoRenameAfterFirstUserMessage(latest)) {
            return { renamed: false, session: latest };
        }
        return {
            renamed: true,
            session: this.rename(id, title),
        };
    }
    delete(id) {
        const sessions = this.loadSessions();
        if (!sessions.some((candidate) => candidate.id === id)) {
            return false;
        }
        const remaining = sessions.filter((candidate) => candidate.id !== id);
        if (remaining.length > 0) {
            this.repository.save(remaining);
            return true;
        }
        this.repository.save([this.createFallbackSession()]);
        return true;
    }
    loadSessions() {
        const sessions = this.repository.list();
        if (this.repository.readCatalog().length === 0) {
            const fallback = this.createFallbackSession();
            this.repository.save([fallback]);
            return [fallback];
        }
        return sessions;
    }
    static activeSessions(sessions) {
        return sessions.filter((session) => !session.archivedAt);
    }
    loadExistingSessions() {
        const catalog = this.repository.readCatalog();
        if (catalog.length > 0) {
            return catalog
                .map((entry) => this.repository.read(entry.id))
                .filter((session) => Boolean(session));
        }
        return [];
    }
    readSession(id) {
        return this.loadSessions().find((candidate) => candidate.id === id);
    }
    updateRequiredSession(id, updater) {
        const updated = this.update(id, updater);
        if (!updated) {
            throw new Error(`Chat session not found: ${id}`);
        }
        return updated;
    }
    createFallbackSession() {
        return ChatSessionRecords.create({
            id: 'session-1',
            name: 'Session 1',
            model: this.config.model,
            reasoningEffort: this.config.reasoningEffort,
            workspaceId: this.config.workspaceId,
            retention: 'reusable',
        });
    }
    static applySettings(session, input) {
        const model = input.model ?? session.model;
        const reasoningEffort = input.reasoningEffort === null ? undefined : input.reasoningEffort ?? session.reasoningEffort;
        const driftEnabled = input.driftEnabled ?? session.driftEnabled;
        if (model === session.model &&
            reasoningEffort === session.reasoningEffort &&
            driftEnabled === session.driftEnabled) {
            return session;
        }
        return {
            ...session,
            model,
            reasoningEffort,
            driftEnabled,
        };
    }
    static normalizeConfig(config) {
        if ('memoryDir' in config && 'traceDir' in config && 'memoryMaintenanceMode' in config) {
            return config;
        }
        const workspaceRoot = resolve(config.workspaceRoot);
        const stateRoot = resolve(config.stateRoot);
        return {
            workspaceRoot,
            stateRoot,
            model: config.model,
            reasoningEffort: config.reasoningEffort,
            sessionStoragePath: resolve(config.sessionStoragePath ?? join(stateRoot, 'chat-sessions.catalog.json')),
            workspaceId: config.workspaceId,
        };
    }
    static getNextSessionNumber(sessions) {
        const highestGenericNumber = sessions.reduce((highest, session) => {
            if (!ChatSessionRecords.isGenericName(session.name)) {
                return highest;
            }
            const parsed = Number.parseInt(session.name.replace(/^Session\s+/, ''), 10);
            if (!Number.isFinite(parsed)) {
                return highest;
            }
            return Math.max(highest, parsed);
        }, 0);
        return highestGenericNumber + 1;
    }
}
//# sourceMappingURL=service.js.map