import { Database } from "bun:sqlite";
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join } from "node:path";

/**
 * Returns the directory for persistent Dispatch data, following XDG Base
 * Directory spec on Linux: `$XDG_DATA_HOME/dispatch` (defaults to
 * `~/.local/share/dispatch`).
 */
function getDataDir(): string {
	const xdg = process.env.XDG_DATA_HOME;
	const base = xdg && isAbsolute(xdg) ? xdg : join(homedir(), ".local", "share");
	return join(base, "dispatch");
}

let _db: Database | null = null;

/**
 * Get (or create) the singleton SQLite database.
 *
 * - Creates the data directory if it doesn't exist.
 * - Creates `dispatch.db` if it doesn't exist.
 * - Enables WAL journal mode for concurrent read performance.
 */
export function getDatabase(): Database {
	if (_db) return _db;

	const dir = getDataDir();
	if (!existsSync(dir)) {
		mkdirSync(dir, { recursive: true });
	}

	const dbPath = join(dir, "dispatch.db");
	_db = new Database(dbPath, { create: true });

	// WAL mode: better concurrent read performance, safe for single-writer
	_db.run("PRAGMA journal_mode = WAL;");
	// Recommended for WAL: normal synchronous is safe and faster
	_db.run("PRAGMA synchronous = NORMAL;");
	// Enable foreign keys
	_db.run("PRAGMA foreign_keys = ON;");

	// Create tables
	_db.run(`CREATE TABLE IF NOT EXISTS credentials (
		key_id            TEXT PRIMARY KEY,
		provider          TEXT NOT NULL,
		access_token      TEXT NOT NULL,
		refresh_token     TEXT NOT NULL,
		expires_at        INTEGER NOT NULL,
		subscription_type TEXT,
		source_file       TEXT,
		imported_at       INTEGER NOT NULL,
		updated_at        INTEGER NOT NULL
	)`);

	// Wake schedule: 4 rows per marked hour (one per :00 / :15 / :30 / :45 probe
	// slot). The PK is (hour, slot_minute). Destructive migration off the legacy
	// single-row-per-hour schema: detect by absence of the `slot_minute` column
	// and drop the old table. Other tables (credentials, api_keys, usage_cache,
	// settings, tabs, chunks) are NOT touched.
	const legacyWakeSchema = (() => {
		try {
			const cols = _db.query("PRAGMA table_info(wake_schedule)").all() as Array<{ name: string }>;
			if (cols.length === 0) return false; // table doesn't exist yet
			return !cols.some((c) => c.name === "slot_minute");
		} catch {
			return false;
		}
	})();
	if (legacyWakeSchema) {
		_db.run("DROP TABLE IF EXISTS wake_schedule");
	}
	_db.run(`CREATE TABLE IF NOT EXISTS wake_schedule (
		hour         INTEGER NOT NULL CHECK (hour BETWEEN 0 AND 23),
		slot_minute  INTEGER NOT NULL CHECK (slot_minute IN (0, 15, 30, 45)),
		next_wake_at INTEGER NOT NULL,
		PRIMARY KEY (hour, slot_minute)
	)`);

	_db.run(`CREATE TABLE IF NOT EXISTS usage_cache (
		key_id      TEXT PRIMARY KEY,
		provider    TEXT NOT NULL,
		cached_at   INTEGER NOT NULL,
		report_json TEXT NOT NULL
	)`);

	_db.run(`CREATE TABLE IF NOT EXISTS api_keys (
		key_id      TEXT PRIMARY KEY,
		provider    TEXT NOT NULL,
		api_key     TEXT NOT NULL,
		imported_at INTEGER NOT NULL,
		updated_at  INTEGER NOT NULL
	)`);

	_db.run(`CREATE TABLE IF NOT EXISTS tabs (
		id             TEXT PRIMARY KEY,
		title          TEXT NOT NULL,
		key_id         TEXT,
		model_id       TEXT,
		parent_tab_id  TEXT,
		status         TEXT NOT NULL DEFAULT 'idle',
		is_open        INTEGER NOT NULL DEFAULT 1,
		position       INTEGER NOT NULL DEFAULT 0,
		created_at     INTEGER NOT NULL,
		updated_at     INTEGER NOT NULL
	)`);

	try {
		_db.run("ALTER TABLE tabs ADD COLUMN parent_tab_id TEXT");
	} catch {
		// Column already exists — ignore
	}

	// ─── Append-only chunk log (replaces the old `messages` blob table) ──
	//
	// A conversation is stored as a flat, append-only stream of chunk rows
	// keyed by a per-tab monotonic `seq`. "Message" and "turn" are DERIVED
	// groupings (see db/chunks.ts), never stored containers. This is what
	// powers per-chunk frontend pagination AND the stable per-step wire
	// format that fixes Anthropic prompt-cache churn (see notes/plan-chunk-log.md).
	//
	//   role  : 'user' | 'assistant' | 'tool' | 'system'
	//   type  : 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'error' | 'system'
	//   step  : LLM round-trip index within a turn (user/system rows = 0)
	//   data_json: the type-specific payload (see ChunkData in types)
	_db.run(`CREATE TABLE IF NOT EXISTS chunks (
		id          TEXT PRIMARY KEY,
		tab_id      TEXT NOT NULL,
		seq         INTEGER NOT NULL,
		turn_id     TEXT NOT NULL,
		step        INTEGER NOT NULL DEFAULT 0,
		role        TEXT NOT NULL,
		type        TEXT NOT NULL,
		data_json   TEXT NOT NULL,
		created_at  INTEGER NOT NULL
	)`);

	_db.run(`CREATE INDEX IF NOT EXISTS idx_chunks_tab_seq ON chunks(tab_id, seq)`);

	// One-shot migration off the legacy `messages` blob model. Beta software,
	// no backward compatibility: the old chat history is destroyed (tabs +
	// messages), while settings / credentials / api_keys / usage_cache /
	// wake_schedule are preserved. Detect the old schema by the presence of
	// the `messages` table; once dropped, this branch never runs again.
	const hasLegacyMessages = _db
		.query("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'")
		.get() as { name: string } | null;
	if (hasLegacyMessages) {
		_db.run("DROP TABLE IF EXISTS messages");
		// Clear conversation containers too (fresh slate for the new model).
		_db.run("DELETE FROM tabs");
		_db.run("DELETE FROM chunks");
	}

	_db.run(`CREATE TABLE IF NOT EXISTS settings (
		key   TEXT PRIMARY KEY,
		value TEXT NOT NULL
	)`);

	return _db;
}

/** Close the database connection (e.g. on shutdown). */
export function closeDatabase(): void {
	if (_db) {
		_db.close();
		_db = null;
	}
}

/** Returns the path where the database file lives (or will live). */
export function getDatabasePath(): string {
	if (_db) return _db.filename;
	const dir = getDataDir();
	return join(dir, "dispatch.db");
}
