import { nanoid } from 'nanoid';
import { DomainError } from './tags.js';
export class ChatService {
    db;
    constructor(db) {
        this.db = db;
    }
    createThread(title = null, opts = {}) {
        const id = nanoid(12);
        const contextType = opts.contextType ?? 'chat';
        const briefPath = opts.briefPath ?? null;
        const patchPath = opts.patchPath ?? null;
        // Invariant L2: context_type='brief' ⇒ brief_path IS NOT NULL.
        if (contextType === 'brief' && !briefPath) {
            throw new DomainError('VALIDATION', "context_type='brief' requires brief_path");
        }
        // M23: context_type='patch' ⇒ patch_path IS NOT NULL.
        if (contextType === 'patch' && !patchPath) {
            throw new DomainError('VALIDATION', "context_type='patch' requires patch_path");
        }
        this.db
            .prepare(`INSERT INTO chat_thread (id, title, context_type, brief_path, patch_path)
         VALUES (?, ?, ?, ?, ?)`)
            .run(id, title, contextType, briefPath, patchPath);
        return this.getThreadRow(id);
    }
    /** M21: list threads attached to a brief (path-keyed lookup). */
    listThreadsForBrief(briefPath) {
        const rows = this.db
            .prepare(`SELECT t.*,
                COUNT(m.id) AS message_count,
                (t.initial_system_prompt IS NOT NULL) AS has_system_prompt
           FROM chat_thread t
           LEFT JOIN chat_message m ON m.thread_id = t.id
          WHERE t.brief_path = ? AND t.context_type = 'brief'
          GROUP BY t.id
          ORDER BY t.updated_at DESC`)
            .all(briefPath);
        return rows.map((r) => ({
            ...this.hydrateThread(r),
            messageCount: r.message_count,
        }));
    }
    /** M23: list threads attached to a patch (path-keyed lookup). */
    listThreadsForPatch(patchPath) {
        const rows = this.db
            .prepare(`SELECT t.*,
                COUNT(m.id) AS message_count,
                (t.initial_system_prompt IS NOT NULL) AS has_system_prompt
           FROM chat_thread t
           LEFT JOIN chat_message m ON m.thread_id = t.id
          WHERE t.patch_path = ? AND t.context_type = 'patch'
          GROUP BY t.id
          ORDER BY t.updated_at DESC`)
            .all(patchPath);
        return rows.map((r) => ({
            ...this.hydrateThread(r),
            messageCount: r.message_count,
        }));
    }
    /** M23: count threads for a patch (cheap version for list UI). */
    threadCountForPatch(patchPath) {
        const row = this.db
            .prepare(`SELECT COUNT(*) AS n FROM chat_thread WHERE patch_path = ? AND context_type = 'patch'`)
            .get(patchPath);
        return row.n;
    }
    listThreads() {
        const rows = this.db
            .prepare(`SELECT t.*,
                COUNT(m.id) AS message_count,
                (t.initial_system_prompt IS NOT NULL) AS has_system_prompt
           FROM chat_thread t
           LEFT JOIN chat_message m ON m.thread_id = t.id
          GROUP BY t.id
          ORDER BY t.updated_at DESC`)
            .all();
        return rows.map((r) => ({
            ...this.hydrateThread(r),
            messageCount: r.message_count,
        }));
    }
    getThread(id, limit, offset) {
        const thread = this.findThread(id);
        if (!thread)
            return null;
        const messages = this.getMessages(id, limit, offset);
        const subagentTasks = this.listSubagentTasks(id);
        return { thread, messages, subagentTasks };
    }
    getThreadMeta(id) {
        return this.findThread(id);
    }
    deleteThread(id) {
        const info = this.db.prepare(`DELETE FROM chat_thread WHERE id = ?`).run(id);
        if (info.changes === 0)
            throw new DomainError('NOT_FOUND', `thread '${id}' not found`);
        return { deleted: true };
    }
    addMessage(threadId, role, content, toolName = null, toolId = null, subagentTaskId = null, planMode = false, status = 'complete') {
        const tx = this.db.transaction(() => {
            const info = this.db
                .prepare(`INSERT INTO chat_message (thread_id, role, content, tool_name, tool_id, subagent_task_id, plan_mode, status)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
                .run(threadId, role, content, toolName, toolId, subagentTaskId, planMode ? 1 : 0, status);
            this.db
                .prepare(`UPDATE chat_thread SET updated_at = datetime('now') WHERE id = ?`)
                .run(threadId);
            const row = this.db
                .prepare(`SELECT * FROM chat_message WHERE id = ?`)
                .get(info.lastInsertRowid);
            return this.hydrateMessage(row);
        });
        return tx();
    }
    startSubagentTask(threadId, taskId, description, toolUseId) {
        this.db
            .prepare(`INSERT INTO chat_subagent_task (thread_id, task_id, tool_use_id, description, status)
         VALUES (?, ?, ?, ?, 'running')
         ON CONFLICT(thread_id, task_id) DO UPDATE SET
           tool_use_id = COALESCE(excluded.tool_use_id, chat_subagent_task.tool_use_id),
           description = excluded.description,
           updated_at  = datetime('now')`)
            .run(threadId, taskId, toolUseId, description);
    }
    updateSubagentTaskProgress(threadId, taskId, description) {
        this.db
            .prepare(`UPDATE chat_subagent_task
            SET description = ?, updated_at = datetime('now')
          WHERE thread_id = ? AND task_id = ?`)
            .run(description, threadId, taskId);
    }
    completeSubagentTask(threadId, taskId, status, summary) {
        this.db
            .prepare(`UPDATE chat_subagent_task
            SET status = ?, summary = ?, updated_at = datetime('now')
          WHERE thread_id = ? AND task_id = ?`)
            .run(status, summary, threadId, taskId);
    }
    listSubagentTasks(threadId) {
        const rows = this.db
            .prepare(`SELECT * FROM chat_subagent_task
          WHERE thread_id = ?
          ORDER BY created_at ASC`)
            .all(threadId);
        return rows.map((r) => this.hydrateSubagentTask(r));
    }
    getMessages(threadId, limit, offset) {
        const rows = limit === undefined
            ? this.db
                .prepare(`SELECT * FROM chat_message
                WHERE thread_id = ?
                ORDER BY id ASC`)
                .all(threadId)
            : this.db
                .prepare(`SELECT * FROM chat_message
                WHERE thread_id = ?
                ORDER BY id ASC
                LIMIT ? OFFSET ?`)
                .all(threadId, limit, offset ?? 0);
        return rows.map((r) => this.hydrateMessage(r));
    }
    updateTitle(threadId, title) {
        const info = this.db
            .prepare(`UPDATE chat_thread SET title = ?, updated_at = datetime('now') WHERE id = ?`)
            .run(title, threadId);
        if (info.changes === 0)
            throw new DomainError('NOT_FOUND', `thread '${threadId}' not found`);
    }
    setLastSessionId(threadId, sessionId) {
        this.db
            .prepare(`UPDATE chat_thread SET last_session_id = ? WHERE id = ?`)
            .run(sessionId, threadId);
    }
    setInitialSystemPrompt(threadId, prompt) {
        this.db
            .prepare(`UPDATE chat_thread SET initial_system_prompt = ?
           WHERE id = ? AND initial_system_prompt IS NULL`)
            .run(prompt, threadId);
    }
    getInitialSystemPrompt(threadId) {
        const row = this.db
            .prepare(`SELECT initial_system_prompt FROM chat_thread WHERE id = ?`)
            .get(threadId);
        return row?.initial_system_prompt ?? null;
    }
    /**
     * M05 session-lock: snapshot `{ model, architectureConfig }` z PIERWSZEJ tury.
     * Punkt odniesienia dla guarda `RESUME_CONFIG_LOCKED` — model i pola reasoningu
     * sa session-immutable. Idempotentny (UPDATE tylko gdy kolumna jest NULL), wiec
     * no-op na 2.+ turze. Wzorowane na `setInitialSystemPrompt`.
     */
    setInitialArchitectureConfig(threadId, snapshot) {
        this.db
            .prepare(`UPDATE chat_thread SET initial_architecture_config_json = ?
           WHERE id = ? AND initial_architecture_config_json IS NULL`)
            .run(JSON.stringify(snapshot), threadId);
    }
    /** Debug-only — NIE w domyslnej projekcji GET /api/threads/:id (jak initial_system_prompt). */
    getInitialArchitectureConfig(threadId) {
        const row = this.db
            .prepare(`SELECT initial_architecture_config_json FROM chat_thread WHERE id = ?`)
            .get(threadId);
        return row?.initial_architecture_config_json ?? null;
    }
    setLastUsage(threadId, usage) {
        this.db
            .prepare(`UPDATE chat_thread SET last_usage_json = ? WHERE id = ?`)
            .run(JSON.stringify(usage), threadId);
    }
    attachTurnUsage(threadId, messageId, usage) {
        this.db
            .prepare(`UPDATE chat_message SET usage_json = ? WHERE id = ? AND thread_id = ?`)
            .run(JSON.stringify(usage), messageId, threadId);
    }
    setLastContextSize(threadId, contextSize) {
        this.db
            .prepare(`UPDATE chat_thread SET last_context_size = ? WHERE id = ?`)
            .run(contextSize, threadId);
    }
    attachTurnContextSize(threadId, messageId, contextSize) {
        this.db
            .prepare(`UPDATE chat_message SET context_size = ? WHERE id = ? AND thread_id = ?`)
            .run(contextSize, messageId, threadId);
    }
    markToolUseComplete(threadId, toolUseId) {
        this.db
            .prepare(`UPDATE chat_message SET status = 'complete'
           WHERE thread_id = ? AND tool_id = ? AND role = 'tool_use'`)
            .run(threadId, toolUseId);
    }
    finalizeStreamingRows(threadId) {
        this.db
            .prepare(`UPDATE chat_message SET status = 'complete'
           WHERE thread_id = ? AND status = 'streaming'`)
            .run(threadId);
    }
    finalizeAllStreamingRows() {
        this.db
            .prepare(`UPDATE chat_message SET status = 'complete' WHERE status = 'streaming'`)
            .run();
    }
    updateCurrentTodoItems(threadId, items) {
        const payload = items && items.length > 0 ? JSON.stringify(items) : null;
        this.db
            .prepare(`UPDATE chat_thread SET current_todo_items = ? WHERE id = ?`)
            .run(payload, threadId);
    }
    updateThreadSettings(threadId, patch) {
        if (patch.planMode !== undefined) {
            const info = this.db
                .prepare(`UPDATE chat_thread SET plan_mode = ?, updated_at = datetime('now') WHERE id = ?`)
                .run(patch.planMode ? 1 : 0, threadId);
            if (info.changes === 0)
                throw new DomainError('NOT_FOUND', `thread '${threadId}' not found`);
        }
        return this.getThreadRow(threadId);
    }
    findThread(id) {
        const row = this.db
            .prepare(`SELECT t.*,
                (t.initial_system_prompt IS NOT NULL) AS has_system_prompt
           FROM chat_thread t
          WHERE t.id = ?`)
            .get(id);
        return row ? this.hydrateThread(row) : null;
    }
    getThreadRow(id) {
        const thread = this.findThread(id);
        if (!thread)
            throw new Error(`thread ${id} disappeared mid-tx`);
        return thread;
    }
    hydrateThread(row) {
        const usage = parseUsage(row.last_usage_json);
        // Backward-compat: stare wątki bez kolumny last_context_size — fallback do
        // usage.inputTokens + usage.outputTokens (mirror `contextSizeOf` z library).
        // Wątki migracja-uwsteczna: zawsze NULL → fallback. Po pierwszej turze post-024
        // server zapisze realny contextSize i fallback przestanie być potrzebny.
        const contextSize = row.last_context_size ?? (usage ? usage.inputTokens + usage.outputTokens : null);
        return {
            id: row.id,
            title: row.title,
            lastSessionId: row.last_session_id,
            currentTodoItems: parseTodoItems(row.current_todo_items),
            planMode: row.plan_mode === 1,
            usage,
            contextSize,
            planId: row.plan_id ?? null,
            lastSeenPlanVersion: row.last_seen_plan_version ?? null,
            hasSystemPrompt: row.has_system_prompt === 1,
            contextType: hydrateContextType(row.context_type),
            briefPath: row.brief_path,
            patchPath: row.patch_path,
            createdAt: row.created_at,
            updatedAt: row.updated_at,
        };
    }
    hydrateMessage(row) {
        return {
            id: row.id,
            threadId: row.thread_id,
            role: row.role,
            content: row.content,
            toolName: row.tool_name,
            toolId: row.tool_id,
            subagentTaskId: row.subagent_task_id,
            planMode: row.plan_mode === 1,
            status: row.status === 'streaming' ? 'streaming' : 'complete',
            usage: parseUsage(row.usage_json),
            contextSize: row.context_size ?? null,
            createdAt: row.created_at,
        };
    }
    hydrateSubagentTask(row) {
        return {
            threadId: row.thread_id,
            taskId: row.task_id,
            toolUseId: row.tool_use_id,
            description: row.description,
            status: row.status,
            summary: row.summary,
            createdAt: row.created_at,
            updatedAt: row.updated_at,
        };
    }
}
/** Map the raw `context_type` column to the typed discriminator. */
function hydrateContextType(raw) {
    return raw === 'brief' || raw === 'patch' ? raw : 'chat';
}
function parseTodoItems(raw) {
    if (!raw)
        return null;
    try {
        const parsed = JSON.parse(raw);
        return Array.isArray(parsed) ? parsed : null;
    }
    catch {
        return null;
    }
}
function parseUsage(raw) {
    if (!raw)
        return null;
    try {
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === 'object')
            return parsed;
        return null;
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=chat.js.map