import Database from 'better-sqlite3';
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import { nanoid } from 'nanoid';
import { normalizeAppSettings } from './app-settings.js';
function now() {
    return new Date().toISOString();
}
function dataHome() {
    return process.env.FATMUX_HOME || join(homedir(), '.fatmux');
}
function parseJsonArray(value) {
    try {
        const parsed = JSON.parse(value);
        return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
    }
    catch {
        return [];
    }
}
function parseMetadata(value) {
    try {
        const parsed = JSON.parse(value);
        return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
    }
    catch {
        return {};
    }
}
function normalizeInteractionMode(value) {
    return value === 'plan' ? 'plan' : 'build';
}
export class FmuxDatabase {
    home;
    file;
    db;
    constructor(home = dataHome()) {
        this.home = home;
        this.file = join(home, 'fatmux.sqlite');
        mkdirSync(home, { recursive: true });
        this.db = new Database(this.file);
        this.db.pragma('journal_mode = WAL');
        this.migrate();
        this.importLegacyProjects();
        this.markEphemeralSessionsClosed();
    }
    close() {
        this.db.close();
    }
    migrate() {
        this.db.exec(`
			CREATE TABLE IF NOT EXISTS projects (
				id TEXT PRIMARY KEY,
				name TEXT NOT NULL,
				path TEXT NOT NULL UNIQUE,
				is_git_repo INTEGER NOT NULL DEFAULT 0,
				created_at TEXT NOT NULL,
				updated_at TEXT NOT NULL,
				favorite INTEGER NOT NULL DEFAULT 0,
				tags TEXT NOT NULL DEFAULT '[]'
			);

			CREATE TABLE IF NOT EXISTS terminals (
				id TEXT PRIMARY KEY,
				project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
				name TEXT NOT NULL,
				alive INTEGER NOT NULL DEFAULT 0,
				created_at TEXT NOT NULL,
				updated_at TEXT NOT NULL,
				exit_code INTEGER,
				history TEXT NOT NULL DEFAULT ''
			);

			CREATE TABLE IF NOT EXISTS agent_sessions (
				id TEXT PRIMARY KEY,
				project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
				provider TEXT NOT NULL CHECK(provider IN ('claude', 'codex')),
				title TEXT NOT NULL,
				status TEXT NOT NULL,
				model TEXT,
				permission_mode TEXT,
				interaction_mode TEXT NOT NULL DEFAULT 'build',
				effort TEXT,
				provider_session_id TEXT,
				created_at TEXT NOT NULL,
				updated_at TEXT NOT NULL,
				last_opened_at TEXT,
				last_error TEXT,
				archived INTEGER NOT NULL DEFAULT 0,
				archived_at TEXT
			);

			CREATE TABLE IF NOT EXISTS agent_messages (
				id TEXT PRIMARY KEY,
				session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
				role TEXT NOT NULL,
				content TEXT NOT NULL,
				event_type TEXT NOT NULL,
				created_at TEXT NOT NULL,
				metadata TEXT NOT NULL DEFAULT '{}'
			);

			CREATE TABLE IF NOT EXISTS agent_attachments (
				id TEXT PRIMARY KEY,
				session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
				message_id TEXT REFERENCES agent_messages(id) ON DELETE SET NULL,
				kind TEXT NOT NULL CHECK(kind IN ('project_file', 'local_upload')),
				name TEXT NOT NULL,
				path TEXT NOT NULL,
				size INTEGER,
				mime_type TEXT,
				created_at TEXT NOT NULL
			);

			CREATE TABLE IF NOT EXISTS agent_events (
				id TEXT PRIMARY KEY,
				session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
				event_type TEXT NOT NULL,
				payload TEXT NOT NULL,
				created_at TEXT NOT NULL
			);

			CREATE TABLE IF NOT EXISTS settings (
				key TEXT PRIMARY KEY,
				value TEXT NOT NULL,
				updated_at TEXT NOT NULL
			);

			CREATE TABLE IF NOT EXISTS project_import_blocks (
				path TEXT PRIMARY KEY,
				created_at TEXT NOT NULL
			);

			CREATE TABLE IF NOT EXISTS agent_provider_settings (
				provider TEXT PRIMARY KEY CHECK(provider IN ('claude', 'codex')),
				model TEXT,
				permission_mode TEXT,
				interaction_mode TEXT NOT NULL DEFAULT 'build',
				effort TEXT,
				updated_at TEXT NOT NULL
			);

			CREATE TABLE IF NOT EXISTS extension_registry_sources (
				kind TEXT NOT NULL CHECK(kind IN ('skill', 'mcp')),
				name TEXT NOT NULL,
				registry TEXT,
				ref TEXT,
				version TEXT,
				url TEXT,
				installed_at TEXT NOT NULL,
				updated_at TEXT NOT NULL,
				PRIMARY KEY (kind, name)
			);

			CREATE TABLE IF NOT EXISTS project_extensions (
				project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
				kind TEXT NOT NULL CHECK(kind IN ('skill', 'mcp')),
				name TEXT NOT NULL,
				enabled INTEGER NOT NULL DEFAULT 1,
				updated_at TEXT NOT NULL,
				PRIMARY KEY (project_id, kind, name)
			);

			CREATE TABLE IF NOT EXISTS agent_session_overrides (
				session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
				kind TEXT NOT NULL CHECK(kind IN ('skill', 'mcp')),
				name TEXT NOT NULL,
				skill_state TEXT,
				mcp_enabled INTEGER,
				updated_at TEXT NOT NULL,
				PRIMARY KEY (session_id, kind, name)
			);
		`);
        this.ensureColumn('agent_sessions', 'interaction_mode', "TEXT NOT NULL DEFAULT 'build'");
        this.ensureColumn('agent_sessions', 'effort', 'TEXT');
        this.ensureColumn('agent_sessions', 'last_opened_at', 'TEXT');
        this.ensureColumn('agent_sessions', 'archived', 'INTEGER NOT NULL DEFAULT 0');
        this.ensureColumn('agent_sessions', 'archived_at', 'TEXT');
        this.ensureProviderSettingsTable();
    }
    ensureColumn(table, column, definition) {
        const rows = this.db.prepare(`PRAGMA table_info(${table})`).all();
        if (rows.some((row) => row.name === column))
            return;
        this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
    }
    ensureProviderSettingsTable() {
        this.db.exec(`
			CREATE TABLE IF NOT EXISTS agent_provider_settings (
				provider TEXT PRIMARY KEY CHECK(provider IN ('claude', 'codex')),
				model TEXT,
				permission_mode TEXT,
				interaction_mode TEXT NOT NULL DEFAULT 'build',
				effort TEXT,
				updated_at TEXT NOT NULL
			);
		`);
    }
    importLegacyProjects() {
        const count = this.db.prepare('SELECT COUNT(*) AS count FROM projects').get();
        if (count.count > 0)
            return;
        const legacyFile = join(this.home, 'projects.json');
        if (!existsSync(legacyFile))
            return;
        try {
            const legacy = JSON.parse(readFileSync(legacyFile, 'utf-8'));
            if (!Array.isArray(legacy))
                return;
            const insert = this.db.prepare(`
				INSERT OR IGNORE INTO projects
				(id, name, path, is_git_repo, created_at, updated_at, favorite, tags)
				VALUES (@id, @name, @path, @isGitRepo, @createdAt, @updatedAt, 0, '[]')
			`);
            const tx = this.db.transaction((items) => {
                for (const item of items) {
                    if (!item.path)
                        continue;
                    const createdAt = item.createdAt || now();
                    insert.run({
                        id: item.id || nanoid(10),
                        name: item.name || item.path.split('/').pop() || item.path,
                        path: item.path,
                        isGitRepo: item.isGitRepo ? 1 : 0,
                        createdAt,
                        updatedAt: createdAt
                    });
                }
            });
            tx(legacy);
        }
        catch {
            // A corrupt legacy project file should not prevent startup.
        }
    }
    markEphemeralSessionsClosed() {
        const stamp = now();
        this.db.prepare('UPDATE terminals SET alive = 0, updated_at = ? WHERE alive = 1').run(stamp);
        this.db
            .prepare("UPDATE agent_sessions SET status = 'error', last_error = 'Server restarted before this run finished', updated_at = ? WHERE status IN ('running', 'queued')")
            .run(stamp);
    }
    projectFromRow(row) {
        return {
            id: row.id,
            name: row.name,
            path: row.path,
            isGitRepo: row.is_git_repo === 1,
            createdAt: row.created_at,
            updatedAt: row.updated_at,
            favorite: row.favorite === 1,
            tags: parseJsonArray(row.tags)
        };
    }
    terminalFromRow(row) {
        return {
            id: row.id,
            projectId: row.project_id,
            name: row.name,
            alive: row.alive === 1,
            createdAt: row.created_at,
            updatedAt: row.updated_at,
            exitCode: row.exit_code
        };
    }
    agentSessionFromRow(row) {
        return {
            id: row.id,
            projectId: row.project_id,
            provider: row.provider,
            title: row.title,
            status: row.status,
            model: row.model,
            permissionMode: row.permission_mode,
            interactionMode: normalizeInteractionMode(row.interaction_mode),
            effort: row.effort,
            providerSessionId: row.provider_session_id,
            createdAt: row.created_at,
            updatedAt: row.updated_at,
            lastOpenedAt: row.last_opened_at,
            lastError: row.last_error,
            archived: row.archived === 1,
            archivedAt: row.archived_at
        };
    }
    agentMessageFromRow(row) {
        return {
            id: row.id,
            sessionId: row.session_id,
            role: row.role,
            content: row.content,
            eventType: row.event_type,
            createdAt: row.created_at,
            metadata: parseMetadata(row.metadata)
        };
    }
    agentAttachmentFromRow(row) {
        return {
            id: row.id,
            sessionId: row.session_id,
            messageId: row.message_id,
            kind: row.kind,
            name: row.name,
            path: row.path,
            size: row.size,
            mimeType: row.mime_type,
            createdAt: row.created_at
        };
    }
    listProjects() {
        const rows = this.db
            .prepare('SELECT * FROM projects ORDER BY favorite DESC, updated_at DESC, name COLLATE NOCASE ASC')
            .all();
        return rows.map((row) => this.projectFromRow(row));
    }
    getProject(id) {
        const row = this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
        return row ? this.projectFromRow(row) : null;
    }
    getProjectByPath(path) {
        const row = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path);
        return row ? this.projectFromRow(row) : null;
    }
    addProject(input) {
        const existing = this.getProjectByPath(input.path);
        if (existing)
            return existing;
        const stamp = now();
        const project = {
            id: nanoid(10),
            name: input.name,
            path: input.path,
            isGitRepo: input.isGitRepo,
            createdAt: stamp,
            updatedAt: stamp,
            favorite: false,
            tags: []
        };
        this.db
            .prepare(`INSERT INTO projects
				(id, name, path, is_git_repo, created_at, updated_at, favorite, tags)
				VALUES (?, ?, ?, ?, ?, ?, 0, '[]')`)
            .run(project.id, project.name, project.path, project.isGitRepo ? 1 : 0, stamp, stamp);
        return project;
    }
    removeProject(id) {
        const result = this.db.prepare('DELETE FROM projects WHERE id = ?').run(id);
        return result.changes > 0;
    }
    listProjectImportBlocks() {
        const rows = this.db
            .prepare('SELECT * FROM project_import_blocks ORDER BY created_at DESC, path COLLATE NOCASE ASC')
            .all();
        return rows.map((row) => ({ path: row.path, createdAt: row.created_at }));
    }
    blockProjectImportPath(path) {
        const normalizedPath = resolve(path);
        const stamp = now();
        this.db
            .prepare('INSERT OR IGNORE INTO project_import_blocks (path, created_at) VALUES (?, ?)')
            .run(normalizedPath, stamp);
        const row = this.db
            .prepare('SELECT * FROM project_import_blocks WHERE path = ?')
            .get(normalizedPath);
        return { path: row.path, createdAt: row.created_at };
    }
    unblockProjectImportPath(path) {
        const result = this.db.prepare('DELETE FROM project_import_blocks WHERE path = ?').run(resolve(path));
        return result.changes > 0;
    }
    isProjectImportBlocked(path) {
        const row = this.db
            .prepare('SELECT 1 AS value FROM project_import_blocks WHERE path = ? LIMIT 1')
            .get(resolve(path));
        return Boolean(row);
    }
    updateProjectGitFlag(id, isGitRepo) {
        this.db
            .prepare('UPDATE projects SET is_git_repo = ?, updated_at = ? WHERE id = ?')
            .run(isGitRepo ? 1 : 0, now(), id);
        return this.getProject(id);
    }
    getAppSettings() {
        const rows = this.db.prepare('SELECT * FROM settings').all();
        const values = new Map(rows.map((row) => [row.key, row.value]));
        return normalizeAppSettings({
            commitMessageProvider: values.get('commitMessageProvider'),
            commitMessageModel: values.get('commitMessageModel') ?? undefined,
            timeFormat: values.get('timeFormat'),
            dateFormat: values.get('dateFormat'),
            telegramBotToken: values.get('telegramBotToken') ?? undefined,
            telegramChatId: values.get('telegramChatId') ?? undefined
        });
    }
    updateAppSettings(patch) {
        const next = normalizeAppSettings({
            ...this.getAppSettings(),
            ...patch
        });
        const stamp = now();
        const upsert = this.db.prepare(`
			INSERT INTO settings (key, value, updated_at)
			VALUES (?, ?, ?)
			ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
		`);
        const tx = this.db.transaction((settings) => {
            upsert.run('commitMessageProvider', settings.commitMessageProvider, stamp);
            upsert.run('commitMessageModel', settings.commitMessageModel, stamp);
            upsert.run('timeFormat', settings.timeFormat, stamp);
            upsert.run('dateFormat', settings.dateFormat, stamp);
            upsert.run('telegramBotToken', settings.telegramBotToken ?? '', stamp);
            upsert.run('telegramChatId', settings.telegramChatId ?? '', stamp);
        });
        tx(next);
        return next;
    }
    createTerminal(input) {
        const stamp = now();
        const session = {
            id: `term_${nanoid(10)}`,
            projectId: input.projectId,
            name: input.name,
            alive: true,
            createdAt: stamp,
            updatedAt: stamp,
            exitCode: null
        };
        this.db
            .prepare(`INSERT INTO terminals
				(id, project_id, name, alive, created_at, updated_at, exit_code, history)
				VALUES (?, ?, ?, 1, ?, ?, NULL, '')`)
            .run(session.id, session.projectId, session.name, stamp, stamp);
        return session;
    }
    getTerminal(id) {
        const row = this.db.prepare('SELECT * FROM terminals WHERE id = ?').get(id);
        return row ? { ...this.terminalFromRow(row), history: row.history } : null;
    }
    listTerminals(projectId) {
        const rows = projectId
            ? this.db
                .prepare('SELECT * FROM terminals WHERE project_id = ? ORDER BY created_at ASC')
                .all(projectId)
            : this.db.prepare('SELECT * FROM terminals ORDER BY created_at ASC').all();
        return rows.map((row) => this.terminalFromRow(row));
    }
    updateTerminalHistory(id, history) {
        this.db.prepare('UPDATE terminals SET history = ?, updated_at = ? WHERE id = ?').run(history, now(), id);
    }
    markTerminalExited(id, exitCode) {
        this.db
            .prepare('UPDATE terminals SET alive = 0, exit_code = ?, updated_at = ? WHERE id = ?')
            .run(exitCode, now(), id);
        const row = this.db.prepare('SELECT * FROM terminals WHERE id = ?').get(id);
        return row ? this.terminalFromRow(row) : null;
    }
    createAgentSession(input) {
        const defaults = this.getAgentProviderSettings(input.provider);
        const stamp = now();
        const session = {
            id: `agent_${nanoid(10)}`,
            projectId: input.projectId,
            provider: input.provider,
            title: input.title,
            status: 'idle',
            model: input.model ?? defaults.model,
            permissionMode: input.permissionMode ?? defaults.permissionMode,
            interactionMode: input.interactionMode ?? defaults.interactionMode,
            effort: input.effort ?? defaults.effort,
            providerSessionId: input.providerSessionId ?? null,
            createdAt: stamp,
            updatedAt: stamp,
            lastOpenedAt: null,
            lastError: null,
            archived: false,
            archivedAt: null
        };
        this.db
            .prepare(`INSERT INTO agent_sessions
				(id, project_id, provider, title, status, model, permission_mode, interaction_mode, effort, provider_session_id, created_at, updated_at, last_opened_at, last_error, archived, archived_at)
				VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, NULL)`)
            .run(session.id, session.projectId, session.provider, session.title, session.status, session.model, session.permissionMode, session.interactionMode, session.effort, session.providerSessionId, stamp, stamp, null);
        return session;
    }
    importAgentSession(input) {
        const session = {
            id: `agent_${nanoid(10)}`,
            projectId: input.projectId,
            provider: input.provider,
            title: input.title,
            status: input.status,
            model: input.model,
            permissionMode: input.permissionMode,
            interactionMode: 'build',
            effort: null,
            providerSessionId: input.providerSessionId,
            createdAt: input.createdAt,
            updatedAt: input.updatedAt,
            lastOpenedAt: null,
            lastError: null,
            archived: false,
            archivedAt: null
        };
        this.db
            .prepare(`INSERT INTO agent_sessions
				(id, project_id, provider, title, status, model, permission_mode, interaction_mode, effort, provider_session_id, created_at, updated_at, last_opened_at, last_error, archived, archived_at)
				VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, NULL)`)
            .run(session.id, session.projectId, session.provider, session.title, session.status, session.model, session.permissionMode, session.interactionMode, session.effort, session.providerSessionId, session.createdAt, session.updatedAt);
        return session;
    }
    addImportedAgentMessage(input) {
        const message = {
            id: `msg_${nanoid(12)}`,
            sessionId: input.sessionId,
            role: input.role,
            content: input.content,
            eventType: input.eventType,
            createdAt: input.createdAt,
            metadata: input.metadata ?? {}
        };
        this.db
            .prepare(`INSERT INTO agent_messages
				(id, session_id, role, content, event_type, created_at, metadata)
				VALUES (?, ?, ?, ?, ?, ?, ?)`)
            .run(message.id, message.sessionId, message.role, message.content, message.eventType, message.createdAt, JSON.stringify(message.metadata));
        return message;
    }
    findAgentSessionByProviderId(provider, providerSessionId) {
        const row = this.db
            .prepare('SELECT * FROM agent_sessions WHERE provider = ? AND provider_session_id = ? LIMIT 1')
            .get(provider, providerSessionId);
        return row ? this.agentSessionFromRow(row) : null;
    }
    transaction(fn) {
        return this.db.transaction(fn)();
    }
    getAgentProviderSettings(provider) {
        const row = this.db
            .prepare('SELECT * FROM agent_provider_settings WHERE provider = ?')
            .get(provider);
        return {
            model: row?.model ?? null,
            permissionMode: row?.permission_mode ?? null,
            interactionMode: normalizeInteractionMode(row?.interaction_mode),
            effort: row?.effort ?? null
        };
    }
    updateAgentProviderSettings(provider, patch) {
        const current = this.getAgentProviderSettings(provider);
        const next = {
            model: Object.hasOwn(patch, 'model') ? patch.model ?? null : current.model,
            permissionMode: Object.hasOwn(patch, 'permissionMode') ? patch.permissionMode ?? null : current.permissionMode,
            interactionMode: Object.hasOwn(patch, 'interactionMode')
                ? normalizeInteractionMode(patch.interactionMode)
                : current.interactionMode,
            effort: Object.hasOwn(patch, 'effort') ? patch.effort ?? null : current.effort
        };
        this.db
            .prepare(`INSERT INTO agent_provider_settings
				(provider, model, permission_mode, interaction_mode, effort, updated_at)
				VALUES (?, ?, ?, ?, ?, ?)
				ON CONFLICT(provider) DO UPDATE SET
					model = excluded.model,
					permission_mode = excluded.permission_mode,
					interaction_mode = excluded.interaction_mode,
					effort = excluded.effort,
					updated_at = excluded.updated_at`)
            .run(provider, next.model, next.permissionMode, next.interactionMode, next.effort, now());
        return next;
    }
    listAgentSessions(projectId, archived = false) {
        const rows = archived === 'all'
            ? this.db
                .prepare('SELECT * FROM agent_sessions WHERE project_id = ? ORDER BY updated_at DESC')
                .all(projectId)
            : this.db
                .prepare('SELECT * FROM agent_sessions WHERE project_id = ? AND archived = ? ORDER BY updated_at DESC')
                .all(projectId, archived ? 1 : 0);
        return rows.map((row) => this.agentSessionFromRow(row));
    }
    getAgentSession(id) {
        const row = this.db
            .prepare('SELECT * FROM agent_sessions WHERE id = ?')
            .get(id);
        return row ? this.agentSessionFromRow(row) : null;
    }
    updateAgentSession(id, patch) {
        const current = this.getAgentSession(id);
        if (!current)
            return null;
        const next = {
            status: Object.hasOwn(patch, 'status') ? patch.status : current.status,
            title: Object.hasOwn(patch, 'title') ? patch.title : current.title,
            model: Object.hasOwn(patch, 'model') ? patch.model : current.model,
            permissionMode: Object.hasOwn(patch, 'permissionMode') ? patch.permissionMode : current.permissionMode,
            interactionMode: Object.hasOwn(patch, 'interactionMode') ? patch.interactionMode : current.interactionMode,
            effort: Object.hasOwn(patch, 'effort') ? patch.effort : current.effort,
            providerSessionId: Object.hasOwn(patch, 'providerSessionId')
                ? patch.providerSessionId
                : current.providerSessionId,
            lastError: Object.hasOwn(patch, 'lastError') ? patch.lastError : current.lastError,
            archived: Object.hasOwn(patch, 'archived') ? patch.archived : current.archived,
            archivedAt: Object.hasOwn(patch, 'archived')
                ? patch.archived
                    ? now()
                    : null
                : current.archivedAt
        };
        this.db
            .prepare(`UPDATE agent_sessions
				SET status = ?, title = ?, model = ?, permission_mode = ?, interaction_mode = ?, effort = ?, provider_session_id = ?, last_error = ?, archived = ?, archived_at = ?, updated_at = ?
				WHERE id = ?`)
            .run(next.status, next.title, next.model, next.permissionMode, next.interactionMode, next.effort, next.providerSessionId, next.lastError, next.archived ? 1 : 0, next.archivedAt, now(), id);
        return this.getAgentSession(id);
    }
    markAgentSessionRead(id) {
        const current = this.getAgentSession(id);
        if (!current)
            return null;
        const lastOpenedAt = current.lastOpenedAt ? Date.parse(current.lastOpenedAt) : Number.NaN;
        const updatedAt = Date.parse(current.updatedAt);
        if (Number.isFinite(lastOpenedAt) && Number.isFinite(updatedAt) && lastOpenedAt >= updatedAt) {
            return current;
        }
        const stamp = now();
        this.db.prepare('UPDATE agent_sessions SET last_opened_at = ? WHERE id = ?').run(stamp, id);
        return this.getAgentSession(id);
    }
    addAgentMessage(input) {
        const message = {
            id: `msg_${nanoid(12)}`,
            sessionId: input.sessionId,
            role: input.role,
            content: input.content,
            eventType: input.eventType,
            createdAt: now(),
            metadata: input.metadata ?? {}
        };
        this.db
            .prepare(`INSERT INTO agent_messages
				(id, session_id, role, content, event_type, created_at, metadata)
				VALUES (?, ?, ?, ?, ?, ?, ?)`)
            .run(message.id, message.sessionId, message.role, message.content, message.eventType, message.createdAt, JSON.stringify(message.metadata));
        this.db.prepare('UPDATE agent_sessions SET updated_at = ? WHERE id = ?').run(now(), input.sessionId);
        return message;
    }
    updateAgentMessage(messageId, patch) {
        const current = this.db
            .prepare('SELECT * FROM agent_messages WHERE id = ?')
            .get(messageId);
        if (!current)
            return null;
        const nextContent = patch.content ?? current.content;
        const nextEventType = patch.eventType ?? current.event_type;
        const nextMetadata = patch.metadata ?? parseMetadata(current.metadata);
        this.db
            .prepare('UPDATE agent_messages SET content = ?, event_type = ?, metadata = ? WHERE id = ?')
            .run(nextContent, nextEventType, JSON.stringify(nextMetadata), messageId);
        this.db.prepare('UPDATE agent_sessions SET updated_at = ? WHERE id = ?').run(now(), current.session_id);
        return this.agentMessageFromRow({
            ...current,
            content: nextContent,
            event_type: nextEventType,
            metadata: JSON.stringify(nextMetadata)
        });
    }
    createAgentAttachment(input) {
        const attachment = {
            id: `att_${nanoid(12)}`,
            sessionId: input.sessionId,
            messageId: input.messageId ?? null,
            kind: input.kind,
            name: input.name,
            path: input.path,
            size: input.size ?? null,
            mimeType: input.mimeType ?? null,
            createdAt: now()
        };
        this.db
            .prepare(`INSERT INTO agent_attachments
				(id, session_id, message_id, kind, name, path, size, mime_type, created_at)
				VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
            .run(attachment.id, attachment.sessionId, attachment.messageId, attachment.kind, attachment.name, attachment.path, attachment.size, attachment.mimeType, attachment.createdAt);
        this.db.prepare('UPDATE agent_sessions SET updated_at = ? WHERE id = ?').run(now(), input.sessionId);
        return attachment;
    }
    listAgentAttachments(sessionId) {
        const rows = this.db
            .prepare('SELECT * FROM agent_attachments WHERE session_id = ? ORDER BY created_at ASC')
            .all(sessionId);
        return rows.map((row) => this.agentAttachmentFromRow(row));
    }
    getAgentAttachment(sessionId, attachmentId) {
        const row = this.db
            .prepare('SELECT * FROM agent_attachments WHERE session_id = ? AND id = ?')
            .get(sessionId, attachmentId);
        return row ? this.agentAttachmentFromRow(row) : null;
    }
    getDraftAgentAttachments(sessionId, attachmentIds) {
        if (attachmentIds.length === 0)
            return [];
        const attachments = attachmentIds.map((id) => this.getAgentAttachment(sessionId, id));
        if (attachments.some((attachment) => !attachment))
            throw new Error('Attachment not found');
        const resolved = attachments;
        if (resolved.some((attachment) => attachment.messageId !== null)) {
            throw new Error('Attachment has already been sent');
        }
        return resolved;
    }
    attachAgentAttachmentsToMessage(sessionId, attachmentIds, messageId) {
        if (attachmentIds.length === 0)
            return;
        const update = this.db.prepare('UPDATE agent_attachments SET message_id = ? WHERE session_id = ? AND id = ? AND message_id IS NULL');
        const tx = this.db.transaction((ids) => {
            for (const id of ids) {
                const result = update.run(messageId, sessionId, id);
                if (result.changes !== 1)
                    throw new Error('Attachment not found');
            }
        });
        tx(attachmentIds);
    }
    deleteAgentAttachment(sessionId, attachmentId) {
        const attachment = this.getAgentAttachment(sessionId, attachmentId);
        if (!attachment)
            return null;
        const result = this.db
            .prepare('DELETE FROM agent_attachments WHERE session_id = ? AND id = ?')
            .run(sessionId, attachmentId);
        return result.changes > 0 ? attachment : null;
    }
    listAgentMessages(sessionId) {
        const rows = this.db
            .prepare('SELECT * FROM agent_messages WHERE session_id = ? ORDER BY created_at ASC')
            .all(sessionId);
        return rows.map((row) => this.agentMessageFromRow(row));
    }
    addAgentEvent(sessionId, eventType, payload) {
        this.db
            .prepare('INSERT INTO agent_events (id, session_id, event_type, payload, created_at) VALUES (?, ?, ?, ?, ?)')
            .run(`event_${nanoid(12)}`, sessionId, eventType, JSON.stringify(payload), now());
    }
    lastProjectActivity(projectId) {
        const row = this.db
            .prepare(`SELECT MAX(value) AS value FROM (
					SELECT updated_at AS value FROM projects WHERE id = ?
					UNION ALL SELECT updated_at FROM terminals WHERE project_id = ?
					UNION ALL SELECT updated_at FROM agent_sessions WHERE project_id = ?
				)`)
            .get(projectId, projectId, projectId);
        return row?.value ?? null;
    }
    upsertExtensionRegistrySource(input) {
        const stamp = now();
        this.db
            .prepare(`INSERT INTO extension_registry_sources (kind, name, registry, ref, version, url, installed_at, updated_at)
				VALUES (?, ?, ?, ?, ?, ?, ?, ?)
				ON CONFLICT(kind, name) DO UPDATE SET
					registry = excluded.registry,
					ref = excluded.ref,
					version = excluded.version,
					url = excluded.url,
					updated_at = excluded.updated_at`)
            .run(input.kind, input.name, input.registry, input.ref, input.version, input.url, stamp, stamp);
    }
    deleteExtensionRegistrySource(kind, name) {
        this.db
            .prepare('DELETE FROM extension_registry_sources WHERE kind = ? AND name = ?')
            .run(kind, name);
    }
    getExtensionRegistrySource(kind, name) {
        const row = this.db
            .prepare('SELECT * FROM extension_registry_sources WHERE kind = ? AND name = ?')
            .get(kind, name);
        if (!row)
            return null;
        return { registry: row.registry, ref: row.ref, version: row.version, url: row.url };
    }
    listProjectExtensions(projectId, kind) {
        const rows = kind
            ? this.db
                .prepare('SELECT * FROM project_extensions WHERE project_id = ? AND kind = ? ORDER BY name ASC')
                .all(projectId, kind)
            : this.db
                .prepare('SELECT * FROM project_extensions WHERE project_id = ? ORDER BY kind ASC, name ASC')
                .all(projectId);
        return rows.map((row) => ({
            projectId: row.project_id,
            kind: row.kind,
            name: row.name,
            enabled: row.enabled === 1
        }));
    }
    listProjectsUsingExtension(kind, name) {
        const rows = this.db
            .prepare('SELECT project_id FROM project_extensions WHERE kind = ? AND name = ? AND enabled = 1')
            .all(kind, name);
        return rows.map((row) => row.project_id);
    }
    setProjectExtension(projectId, kind, name, enabled) {
        const stamp = now();
        this.db
            .prepare(`INSERT INTO project_extensions (project_id, kind, name, enabled, updated_at)
				VALUES (?, ?, ?, ?, ?)
				ON CONFLICT(project_id, kind, name) DO UPDATE SET
					enabled = excluded.enabled,
					updated_at = excluded.updated_at`)
            .run(projectId, kind, name, enabled ? 1 : 0, stamp);
    }
    deleteProjectExtension(projectId, kind, name) {
        this.db
            .prepare('DELETE FROM project_extensions WHERE project_id = ? AND kind = ? AND name = ?')
            .run(projectId, kind, name);
    }
    deleteExtensionEverywhere(kind, name) {
        this.db.prepare('DELETE FROM project_extensions WHERE kind = ? AND name = ?').run(kind, name);
        this.db
            .prepare('DELETE FROM agent_session_overrides WHERE kind = ? AND name = ?')
            .run(kind, name);
        this.db
            .prepare('DELETE FROM extension_registry_sources WHERE kind = ? AND name = ?')
            .run(kind, name);
    }
    listSessionExtensionOverrides(sessionId) {
        const rows = this.db
            .prepare('SELECT * FROM agent_session_overrides WHERE session_id = ?')
            .all(sessionId);
        return rows.map((row) => ({
            sessionId: row.session_id,
            kind: row.kind,
            name: row.name,
            skillState: row.skill_state,
            mcpEnabled: row.mcp_enabled === null ? null : row.mcp_enabled === 1
        }));
    }
    setSessionExtensionOverride(input) {
        const stamp = now();
        const skillState = input.kind === 'skill' ? input.skillState ?? null : null;
        const mcpEnabled = input.kind === 'mcp' ? (input.mcpEnabled === null || input.mcpEnabled === undefined ? null : input.mcpEnabled ? 1 : 0) : null;
        this.db
            .prepare(`INSERT INTO agent_session_overrides (session_id, kind, name, skill_state, mcp_enabled, updated_at)
				VALUES (?, ?, ?, ?, ?, ?)
				ON CONFLICT(session_id, kind, name) DO UPDATE SET
					skill_state = excluded.skill_state,
					mcp_enabled = excluded.mcp_enabled,
					updated_at = excluded.updated_at`)
            .run(input.sessionId, input.kind, input.name, skillState, mcpEnabled, stamp);
    }
    deleteSessionExtensionOverride(sessionId, kind, name) {
        this.db
            .prepare('DELETE FROM agent_session_overrides WHERE session_id = ? AND kind = ? AND name = ?')
            .run(sessionId, kind, name);
    }
}
//# sourceMappingURL=database.js.map