import { l as errorMessage } from "../errors-DkR6GPJw.js";
import { dirname } from "node:path";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { Database } from "bun:sqlite";
//#region src/session/sqlite.ts
const SCHEMA_VERSION = 4;
function createSqliteStore(options) {
	const db = new Database(options.path);
	db.run("PRAGMA busy_timeout = 5000");
	db.run("PRAGMA journal_mode = WAL");
	db.run("PRAGMA foreign_keys = ON");
	db.run(`
    CREATE TABLE IF NOT EXISTS sessions (
      id TEXT PRIMARY KEY,
      agent_id TEXT,
      project_root TEXT,
      status TEXT NOT NULL DEFAULT 'idle',
      metadata TEXT NOT NULL DEFAULT '{}',
      created_at INTEGER NOT NULL,
      updated_at INTEGER NOT NULL
    )
  `);
	db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id)`);
	db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_project_root ON sessions(project_root)`);
	db.run(`
    CREATE TABLE IF NOT EXISTS turns (
      session_id TEXT NOT NULL,
      seq INTEGER NOT NULL,
      data TEXT NOT NULL,
      PRIMARY KEY (session_id, seq),
      FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
    )
  `);
	db.run(`
    CREATE TABLE IF NOT EXISTS runs (
      session_id TEXT NOT NULL,
      run_id TEXT NOT NULL,
      started_at INTEGER NOT NULL,
      data TEXT NOT NULL,
      PRIMARY KEY (session_id, run_id),
      FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
    )
  `);
	migrate(db, options.path);
	const stmtLoadHeader = db.prepare(`
    SELECT id, agent_id, project_root, status, metadata, created_at, updated_at
    FROM sessions WHERE id = ?
  `);
	const stmtLoadTurns = db.prepare("SELECT data FROM turns WHERE session_id = ? ORDER BY seq");
	const stmtLoadTurnsRange = db.prepare("SELECT data FROM turns WHERE session_id = ? ORDER BY seq LIMIT ? OFFSET ?");
	const stmtLoadRuns = db.prepare("SELECT data FROM runs WHERE session_id = ? ORDER BY started_at, run_id");
	const stmtUpsertHeader = db.prepare(`
    INSERT INTO sessions (id, agent_id, project_root, status, metadata, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(id) DO UPDATE SET
      agent_id = excluded.agent_id,
      project_root = excluded.project_root,
      status = excluded.status,
      metadata = excluded.metadata,
      updated_at = excluded.updated_at
  `);
	const stmtDelete = db.prepare("DELETE FROM sessions WHERE id = ?");
	const stmtDeleteTurnsForSession = db.prepare("DELETE FROM turns WHERE session_id = ?");
	const stmtDeleteRunsForSession = db.prepare("DELETE FROM runs WHERE session_id = ?");
	const stmtInsertTurn = db.prepare("INSERT INTO turns (session_id, seq, data) VALUES (?, ?, ?)");
	const stmtUpsertRun = db.prepare(`
    INSERT INTO runs (session_id, run_id, started_at, data) VALUES (?, ?, ?, ?)
    ON CONFLICT(session_id, run_id) DO UPDATE SET
      data = excluded.data,
      started_at = excluded.started_at
  `);
	const stmtMaxSeq = db.prepare("SELECT MAX(seq) as m FROM turns WHERE session_id = ?");
	const stmtTouch = db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?");
	const stmtUpdateStatus = db.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?");
	const loadSync = (sessionId) => {
		const header = stmtLoadHeader.get(sessionId);
		if (!header) return null;
		const turnRows = stmtLoadTurns.all(sessionId);
		const runRows = stmtLoadRuns.all(sessionId);
		return {
			id: header.id,
			...header.agent_id ? { agentId: header.agent_id } : {},
			...header.project_root ? { projectRoot: header.project_root } : {},
			turns: turnRows.map((r) => JSON.parse(r.data)),
			runs: runRows.map((r) => JSON.parse(r.data)),
			status: header.status,
			metadata: safeParseRecord(header.metadata),
			createdAt: header.created_at,
			updatedAt: header.updated_at
		};
	};
	const txnSave = db.transaction((session) => {
		stmtUpsertHeader.run(session.id, session.agentId ?? null, session.projectRoot ?? null, session.status, JSON.stringify(session.metadata ?? {}), session.createdAt, session.updatedAt);
		stmtDeleteTurnsForSession.run(session.id);
		stmtDeleteRunsForSession.run(session.id);
		session.turns.forEach((t, i) => stmtInsertTurn.run(session.id, i, JSON.stringify(t)));
		for (const run of session.runs) stmtUpsertRun.run(session.id, run.id, run.startedAt, JSON.stringify(run));
	});
	const ensureHeaderSync = (sessionId) => {
		if (stmtLoadHeader.get(sessionId)) return;
		const now = Date.now();
		stmtUpsertHeader.run(sessionId, null, null, "idle", "{}", now, now);
	};
	const txnAppendTurns = db.transaction((sessionId, turns) => {
		ensureHeaderSync(sessionId);
		let nextSeq = (stmtMaxSeq.get(sessionId).m ?? -1) + 1;
		for (const turn of turns) {
			stmtInsertTurn.run(sessionId, nextSeq, JSON.stringify(turn));
			nextSeq += 1;
		}
		stmtTouch.run(Date.now(), sessionId);
	});
	const txnUpdateRun = db.transaction((sessionId, run) => {
		if (!stmtLoadHeader.get(sessionId)) return;
		stmtUpsertRun.run(sessionId, run.id, run.startedAt, JSON.stringify(run));
		stmtTouch.run(Date.now(), sessionId);
	});
	const txnUpdateStatus = db.transaction((sessionId, status) => {
		stmtUpdateStatus.run(status, Date.now(), sessionId);
	});
	return {
		async load(sessionId) {
			return loadSync(sessionId);
		},
		async save(session) {
			txnSave.immediate(session);
		},
		async delete(sessionId) {
			stmtDelete.run(sessionId);
		},
		async list(filter) {
			const conditions = [];
			const params = [];
			if (filter?.agentId) {
				conditions.push("agent_id = ?");
				params.push(filter.agentId);
			}
			if ("projectRoot" in (filter ?? {})) {
				const v = filter.projectRoot;
				if (v === null) conditions.push("project_root IS NULL");
				else if (typeof v === "string") {
					conditions.push("project_root = ?");
					params.push(v);
				}
			}
			const sql = `SELECT id FROM sessions ${conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""} ORDER BY updated_at DESC ${typeof filter?.limit === "number" && filter.limit > 0 ? `LIMIT ${filter.limit}` : ""}`.replace(/\s+/g, " ").trim();
			return db.query(sql).all(...params).map((r) => r.id);
		},
		async appendTurns(sessionId, turns) {
			txnAppendTurns.immediate(sessionId, turns);
		},
		async getTurns(sessionId, from = 0, limit) {
			const lim = typeof limit === "number" ? limit : -1;
			return stmtLoadTurnsRange.all(sessionId, lim, from).map((r) => JSON.parse(r.data));
		},
		async updateRun(sessionId, run) {
			txnUpdateRun.immediate(sessionId, run);
		},
		async updateStatus(sessionId, status) {
			txnUpdateStatus.immediate(sessionId, status);
		}
	};
}
/**
* Forward-only schema migration runner. Idempotent — every step
* feature-detects rather than relying on the `PRAGMA user_version`
* number alone, so a partially-migrated DB (e.g. interrupted v3→v4
* backfill) is brought to a consistent v4 state on the next open.
*
* The legacy `data` column is the v3 sentinel: when present we treat
* the DB as v3-or-older and run the backfill. Before any destructive
* change we copy the entire DB file to `<path>.zidane-pre-v4.bak` (see
* {@link backupBeforeV3Migration}) so the user has a guaranteed
* recovery path if anything goes sideways.
*
* After backfill we attempt to drop the column (SQLite 3.35+; Bun
* ships ≥ 3.43 so this always succeeds in practice). If it does fail,
* we throw rather than limp along — leaving the column with `NOT NULL`
* while the new write paths omit it would silently break every future
* `INSERT`. The `.bak` file lets the user roll back and report the bug.
*/
function migrate(db, dbPath) {
	const sessionCols = db.query("PRAGMA table_info(sessions)").all();
	const hasCol = (n) => sessionCols.some((c) => c.name === n);
	if (!hasCol("project_root")) db.run("ALTER TABLE sessions ADD COLUMN project_root TEXT");
	if (!hasCol("status")) db.run(`ALTER TABLE sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'idle'`);
	if (!hasCol("metadata")) db.run(`ALTER TABLE sessions ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'`);
	if (hasCol("data")) {
		backupBeforeV3Migration(db, dbPath);
		backfillV3Blobs(db);
		try {
			db.run("ALTER TABLE sessions DROP COLUMN data");
		} catch (err) {
			throw new Error(`[zidane/sqlite] v3→v4 migration could not drop legacy "data" column: ${errorMessage(err)}. Restore from "${dbPath}.zidane-pre-v4.bak" with an older zidane build and report this — your sessions are intact in the backup file.`);
		}
	}
	if ((db.query("PRAGMA user_version").get()?.user_version ?? 0) < SCHEMA_VERSION) db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
}
/**
* Pre-migration safety net for v3 → v4: copy the source DB file to
* `<path>.zidane-pre-v4.bak` so the user can recover by restoring the
* snapshot if anything in the migration goes sideways.
*
*  - Skipped for non-filesystem paths (`:memory:`, empty string) — no
*    file to copy.
*  - Skipped when the file is missing (fresh install, never reached
*    migration territory).
*  - Skipped when the backup already exists — re-runs of the migration
*    (idempotent path) reuse the same snapshot rather than overwrite a
*    potentially-earlier good copy with a later partial state.
*  - WAL is checkpointed first so the .db file contains everything that
*    was committed up to this point (otherwise pending WAL frames would
*    be missing from the snapshot).
*  - On copy failure the whole migration is aborted with a thrown error
*    — we refuse to modify the DB without a recovery path. The user
*    sees a clear diagnostic; their sessions stay intact.
*
* Surfaces a one-line stderr message on success so the user knows where
* the snapshot landed. They can delete it once they've confirmed the
* post-migration TUI behaves correctly.
*/
function backupBeforeV3Migration(db, dbPath) {
	if (dbPath === "" || dbPath === ":memory:" || dbPath.startsWith("file::memory:")) return;
	if (!existsSync(dbPath)) return;
	const backupPath = `${dbPath}.zidane-pre-v4.bak`;
	if (existsSync(backupPath)) return;
	try {
		db.run("PRAGMA wal_checkpoint(TRUNCATE)");
	} catch {}
	try {
		copyFileSync(dbPath, backupPath);
		process.stderr.write(`[zidane/sqlite] v3→v4 migration: backed up your sessions DB to "${backupPath}".\n  To roll back if anything looks wrong:\n    1. quit zidane\n    2. cp "${backupPath}" "${dbPath}" && rm -f "${dbPath}-wal" "${dbPath}-shm"\n    3. pin an older zidane that knows the v3 schema, or upgrade to a fix\n  Delete the .bak once every session loads correctly.\n`);
	} catch (err) {
		throw new Error(`[zidane/sqlite] failed to back up "${dbPath}" before v3→v4 migration: ${errorMessage(err)}. Refusing to modify the DB without a recovery path. Move the file aside or fix the permissions and retry.`);
	}
}
/**
* Parse each session's legacy `data` blob and fan its `turns` + `runs`
* arrays out into the dedicated tables, then overwrite the synthetic
* `status`/`metadata` defaults with the values from the blob.
*
* Skips rows that already have at least one entry in `turns` for them —
* a previous run of this migration already landed those bytes, and
* re-inserting them would either duplicate (if seq numbering shifted)
* or crash on the PK. The "header already exists, turns table populated"
* combination is the idempotency signal.
*
* Per-row failures (corrupt JSON, …) log + skip rather than abort the
* whole migration: one bad row shouldn't prevent the rest of the DB
* from coming forward to v4.
*/
function backfillV3Blobs(db) {
	const rows = db.query("SELECT id, data FROM sessions").all();
	if (rows.length === 0) return;
	const insertTurn = db.prepare("INSERT INTO turns (session_id, seq, data) VALUES (?, ?, ?)");
	const upsertRun = db.prepare(`
    INSERT INTO runs (session_id, run_id, started_at, data) VALUES (?, ?, ?, ?)
    ON CONFLICT(session_id, run_id) DO UPDATE SET
      data = excluded.data,
      started_at = excluded.started_at
  `);
	const updateHeader = db.prepare("UPDATE sessions SET status = ?, metadata = ? WHERE id = ?");
	const countTurns = db.prepare("SELECT COUNT(*) as c FROM turns WHERE session_id = ?");
	for (const row of rows) {
		if (!row.data) continue;
		try {
			const data = JSON.parse(row.data);
			if (countTurns.get(row.id).c > 0) continue;
			const turns = Array.isArray(data.turns) ? data.turns : [];
			const runs = Array.isArray(data.runs) ? data.runs : [];
			db.transaction(() => {
				turns.forEach((t, i) => insertTurn.run(row.id, i, JSON.stringify(t)));
				for (const run of runs) {
					if (!run || typeof run.id !== "string") continue;
					upsertRun.run(row.id, run.id, run.startedAt ?? 0, JSON.stringify(run));
				}
				updateHeader.run(data.status ?? "idle", JSON.stringify(data.metadata ?? {}), row.id);
			}).immediate();
		} catch (err) {
			process.stderr.write(`[zidane/sqlite] v3→v4 migrate: session "${row.id}" skipped: ${errorMessage(err)}\n`);
		}
	}
}
/**
* `JSON.parse` with a fallback to `{}` for the metadata column. v3→v4
* migration writes well-formed JSON, fresh v4 writes go through
* `JSON.stringify(metadata ?? {})` — but a hand-edited DB or a botched
* external migration could leave a row with malformed JSON, and we'd
* rather surface an empty metadata object than crash the entire load
* path. Logged under `ZIDANE_DEBUG` so the corruption isn't fully
* silent.
*/
function safeParseRecord(text) {
	try {
		const parsed = JSON.parse(text);
		return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
	} catch (err) {
		if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/sqlite] metadata JSON parse failed, defaulting to {}: ${errorMessage(err)}\n`);
		return {};
	}
}
/**
* Convenience: open a SQLite store at `dbPath`, creating its parent
* directory if it doesn't yet exist. Used by the TUI launcher to spin up
* the default session store at `~/.zidane/sessions.db`.
*
* Lives here (and not in `chat/store.ts`) so the renderer-agnostic
* `zidane/chat` entry stays free of `bun:sqlite` — non-Bun consumers
* (a future GUI, an SDK) can import `zidane/chat` without pulling in the
* Bun-only binding.
*/
function createTuiStore(dbPath) {
	const dir = dirname(dbPath);
	if (!existsSync(dir)) try {
		mkdirSync(dir, { recursive: true });
	} catch (err) {
		throw new Error(`Could not create TUI storage directory at "${dir}". Override the location via \`runTui({ storageDir, prefix })\` or the \`ZIDANE_STORAGE_DIR\` env var. Original error: ${errorMessage(err)}`);
	}
	return createSqliteStore({ path: dbPath });
}
//#endregion
export { createSqliteStore, createTuiStore };

//# sourceMappingURL=sqlite.js.map