/**
 * @module        services/ChatService
 * @description   Chat 会话 CRUD service
 *
 * @layer         services
 *
 * Phase 2 范围：
 *   - session 生命周期（list / create / get / delete）
 *   - messages diff 查询
 *
 * Phase 3 追加：
 *   - sendMessage（走 daemon IPC + SSE 流式推送）—— 通过注入 ChatExecutor port
 *
 * 本 service 隐藏 chat session JSON 文件的存储细节，使用 FileSystem port。
 */
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
import { jsonlExists, parseJsonlMessages, summarizeJsonl, } from '../core/claudeJsonl.js';
import { domainError } from '../shared/errors.js';
import { err, ok } from '../shared/result.js';
import { chatSessionsDir, sessionsDir } from '../shared/runtimePaths.js';
export class ChatService {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    async list() {
        const summaries = [];
        // 1) Console 会话 —— ~/.coral/chat-sessions/<id>.json
        const dir = chatSessionsDir();
        const consoleIds = new Set();
        if (this.deps.fs.exists(dir)) {
            let files;
            try {
                files = this.deps.fs.readDir(dir).filter((e) => e.isFile && e.name.endsWith('.json'));
            }
            catch (cause) {
                return err(domainError('internal', 'CHAT_LIST_FAIL', 'Failed to read session list', { cause }));
            }
            for (const entry of files) {
                const path = resolve(dir, entry.name);
                try {
                    const raw = this.deps.fs.readFile(path);
                    const session = JSON.parse(raw);
                    summaries.push({ ...this.summarize(session), source: 'console', readonly: false });
                    consoleIds.add(session.id);
                }
                catch {
                    /* 跳过坏 session */
                }
            }
        }
        // 2) CLI 会话 —— daemon state.json + Claude jsonl 历史
        //    跳过 Console 用 `session-chat-<consoleId>` 占的 slot，避免重复
        for (const cli of this.readDaemonSessions()) {
            const consoleId = stripChatPrefix(cli.slot);
            if (consoleId && consoleIds.has(consoleId))
                continue;
            if (!jsonlExists(cli.cwd, cli.sessionId))
                continue;
            const summary = summarizeJsonl(cli.cwd, cli.sessionId);
            summaries.push({
                id: cli.slot,
                createdAt: cli.createdAt,
                lastMessageAt: summary.lastMessageAt ?? cli.lastSeenAt ?? cli.updatedAt,
                title: summary.firstUserText || stripSessionPrefix(cli.slot),
                project: null,
                messageCount: summary.messageCount,
                cwd: cli.cwd,
                source: 'cli',
                slot: cli.slot,
                sessionState: cli.sessionState,
                readonly: true,
            });
        }
        summaries.sort((a, b) => {
            const ta = a.lastMessageAt ?? a.createdAt;
            const tb = b.lastMessageAt ?? b.createdAt;
            return Date.parse(tb) - Date.parse(ta);
        });
        return ok(summaries);
    }
    /** 读 daemon state.json 取 sessions —— 文件不存在或损坏返回空数组 */
    readDaemonSessions() {
        const path = resolve(sessionsDir(), 'state.json');
        if (!this.deps.fs.exists(path))
            return [];
        try {
            const raw = this.deps.fs.readFile(path);
            const parsed = JSON.parse(raw);
            return Object.values(parsed.sessions ?? {});
        }
        catch {
            return [];
        }
    }
    async create(input = {}) {
        // v0.51.4: validate cwd if provided
        let cwd = null;
        if (input.cwd != null && input.cwd.trim() !== '') {
            const trimmed = input.cwd.trim();
            const validation = validateCwd(trimmed, this.deps.fs);
            if (!validation.ok)
                return validation;
            cwd = validation.value;
        }
        const id = randomUUID();
        const session = {
            id,
            createdAt: this.deps.clock.nowIso(),
            lastMessageAt: null,
            title: input.title?.trim() || 'New chat',
            project: input.project ?? null,
            messageCount: 0,
            messages: [],
            cwd,
        };
        try {
            this.deps.fs.writeFileAtomic(this.sessionPath(id), JSON.stringify(session));
        }
        catch (cause) {
            return err(domainError('internal', 'CHAT_CREATE_FAIL', 'Failed to create session', { cause }));
        }
        return ok(this.summarize(session));
    }
    async get(id) {
        if (!isValidSessionId(id))
            return err(invalidId());
        // 1) Console session —— chat-sessions/<id>.json
        const path = this.sessionPath(id);
        if (this.deps.fs.exists(path)) {
            try {
                const raw = this.deps.fs.readFile(path);
                return ok(JSON.parse(raw));
            }
            catch (cause) {
                return err(domainError('internal', 'CHAT_READ_FAIL', 'Failed to read session', { cause }));
            }
        }
        // 2) CLI session —— id 是 daemon slot 名，从 jsonl 实时解析
        if (id.startsWith('session-')) {
            const cli = this.readDaemonSessions().find((s) => s.slot === id);
            if (cli && jsonlExists(cli.cwd, cli.sessionId)) {
                const messages = parseJsonlMessages(cli.cwd, cli.sessionId);
                const lastTs = messages.length > 0 ? messages[messages.length - 1].ts : null;
                const firstUser = messages.find((m) => m.role === 'user' && m.content.trim());
                return ok({
                    id,
                    createdAt: cli.createdAt,
                    lastMessageAt: lastTs ?? cli.lastSeenAt ?? cli.updatedAt,
                    title: firstUser?.content.trim().slice(0, 120) || stripSessionPrefix(id),
                    project: null,
                    messageCount: messages.length,
                    messages,
                    cwd: cli.cwd,
                    source: 'cli',
                    readonly: true,
                });
            }
        }
        return err(domainError('not-found', 'CHAT_SESSION_NOT_FOUND', 'Session not found'));
    }
    /** 返回 since 之后的消息（供 SSE 重连补偿）。since 缺省返回全部。 */
    async getMessages(id, since) {
        const sessionR = await this.get(id);
        if (!sessionR.ok)
            return sessionR;
        const session = sessionR.value;
        const messages = since ? session.messages.filter((m) => m.ts > since) : session.messages;
        return ok({ data: messages, total: session.messages.length });
    }
    async delete(id) {
        if (!isValidSessionId(id))
            return err(invalidId());
        // CLI 会话：停 daemon slot + 从 state.json 移除；保留 Claude jsonl 存档（Claude 自己的文件）。
        if (id.startsWith('session-')) {
            if (this.deps.executor) {
                try {
                    await this.deps.executor.stopExactSlot(id);
                }
                catch {
                    /* daemon 没起或 slot 已不在，照样推进 */
                }
            }
            try {
                this.removeDaemonSession(id);
            }
            catch (cause) {
                return err(domainError('internal', 'CHAT_DELETE_FAIL', 'Failed to remove session from daemon state', { cause }));
            }
            return ok(undefined);
        }
        const path = this.sessionPath(id);
        if (this.deps.fs.exists(path)) {
            try {
                this.deps.fs.unlink(path);
            }
            catch (cause) {
                return err(domainError('internal', 'CHAT_DELETE_FAIL', 'Failed to delete session', { cause }));
            }
        }
        // best effort 停止 daemon worker
        if (this.deps.executor) {
            try {
                await this.deps.executor.stopSession(id);
            }
            catch {
                /* 不阻塞删除 */
            }
        }
        return ok(undefined);
    }
    /**
     * 从 daemon state.json 里把 slot 删掉，让 list() 不再返回。
     * jsonl 文件刻意留着 —— 那是 Claude CLI 自己的存档，跨边界删除越权。
     */
    removeDaemonSession(slot) {
        const path = resolve(sessionsDir(), 'state.json');
        if (!this.deps.fs.exists(path))
            return;
        let parsed;
        try {
            parsed = JSON.parse(this.deps.fs.readFile(path));
        }
        catch {
            // 损坏的 state.json：交给 daemon 自己重建，这里不动
            return;
        }
        if (!parsed.sessions || !parsed.sessions[slot])
            return;
        delete parsed.sessions[slot];
        parsed.updatedAt = this.deps.clock.nowIso();
        parsed.updatedBy = 'chat-service-delete';
        this.deps.fs.writeFileAtomic(path, JSON.stringify(parsed, null, 2));
    }
    async interrupt(id) {
        if (!isValidSessionId(id))
            return err(invalidId());
        if (id.startsWith('session-')) {
            return err(domainError('validation', 'CHAT_CLI_READ_ONLY', 'CLI sessions are read-only and cannot be interrupted from the Console'));
        }
        if (!this.deps.executor) {
            return err(domainError('internal', 'EXECUTOR_MISSING', 'ChatExecutor not injected'));
        }
        try {
            await this.deps.executor.cancelRun(id);
        }
        catch (cause) {
            return err(domainError('external', 'CHAT_CANCEL_FAIL', 'Failed to interrupt session', { cause }));
        }
        return ok(undefined);
    }
    // ─── helpers ──────────────────────────────────────────────────────
    sessionPath(id) {
        return resolve(chatSessionsDir(), `${id}.json`);
    }
    summarize(s) {
        return {
            id: s.id,
            createdAt: s.createdAt,
            lastMessageAt: s.lastMessageAt,
            title: s.title,
            project: s.project,
            messageCount: s.messageCount,
            cwd: s.cwd ?? null,
        };
    }
}
function isValidSessionId(id) {
    return typeof id === 'string' && /^[a-zA-Z0-9_-]+$/.test(id);
}
/** Console 占的 daemon slot 形如 `session-chat-<consoleId>` —— 抽出 consoleId 用于去重 */
function stripChatPrefix(slot) {
    const m = /^session-chat-(.+)$/.exec(slot);
    return m ? m[1] : null;
}
/** `session-foo` → `foo`，给 CLI 会话提供个简短 fallback title */
function stripSessionPrefix(slot) {
    return slot.replace(/^session-/, '');
}
function invalidId() {
    return domainError('validation', 'INVALID_SESSION_ID', 'Invalid session id');
}
/**
 * v0.51.4: validate that cwd is absolute, exists on disk, and isn't pointing
 * at something dangerous. Returns a normalized absolute path on success.
 */
function validateCwd(cwd, fs) {
    // Must be absolute (no relative paths — daemon would resolve them against its
    // own cwd, which defeats the purpose of letting users pick).
    if (!cwd.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(cwd)) {
        return err(domainError('validation', 'CHAT_CWD_NOT_ABSOLUTE', 'Working directory must be an absolute path', {
            details: { cwd },
        }));
    }
    if (!fs.exists(cwd)) {
        return err(domainError('validation', 'CHAT_CWD_NOT_FOUND', `Working directory does not exist: ${cwd}`, {
            details: { cwd },
        }));
    }
    return ok(cwd);
}
//# sourceMappingURL=ChatService.js.map