import Database from 'better-sqlite3';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { getThemePersistentDir, isEphemeralAgentName } from '../themes.js';
import { PMO_SCHEMA_SQL } from '../pmo/schema.js';
export const CREATE_TABLES_SQL = `
-- Core workspace metadata
CREATE TABLE IF NOT EXISTS workspace (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  type TEXT NOT NULL CHECK (type IN ('hq', 'workspace')),
  workspace_name TEXT NOT NULL,
  has_pmo BOOLEAN DEFAULT FALSE,
  active_theme_id TEXT,
  created_at TEXT NOT NULL,
  FOREIGN KEY (active_theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL
);

-- Repository management
CREATE TABLE IF NOT EXISTS repositories (
  name TEXT PRIMARY KEY,
  path TEXT NOT NULL,
  type TEXT DEFAULT 'main' CHECK (type IN ('main', 'dependency')),
  source_url TEXT,
  action TEXT CHECK (action IN ('clone', 'move', 'link')),
  added_at TEXT NOT NULL
);

-- Agent naming themes (optional)
CREATE TABLE IF NOT EXISTS agent_themes (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL UNIQUE,
  display_name TEXT NOT NULL,
  description TEXT,
  builtin BOOLEAN DEFAULT FALSE,
  created_at TEXT NOT NULL
);

-- Names available within each theme
CREATE TABLE IF NOT EXISTS agent_theme_names (
  theme_id TEXT NOT NULL,
  name TEXT NOT NULL,
  PRIMARY KEY (theme_id, name),
  FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
);

-- Agent instances in workspace
CREATE TABLE IF NOT EXISTS agents (
  name TEXT PRIMARY KEY,
  type TEXT NOT NULL DEFAULT 'persistent' CHECK (type IN ('persistent', 'ephemeral')),
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'cleaned')),
  base_name TEXT,
  theme_id TEXT,
  worktree_path TEXT,
  mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone')),
  created_at TEXT NOT NULL,
  cleaned_at TEXT,
  FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL
);

-- Agent-owned worktrees
CREATE TABLE IF NOT EXISTS agent_worktrees (
  agent_name TEXT NOT NULL,
  repo_name TEXT NOT NULL,
  worktree_path TEXT NOT NULL,
  branch TEXT NOT NULL,
  created_at TEXT NOT NULL,
  last_commit_hash TEXT,
  commits_ahead INTEGER NOT NULL DEFAULT 0,
  is_clean INTEGER NOT NULL DEFAULT 1,
  last_checked TEXT,
  PRIMARY KEY (agent_name, repo_name),
  FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,
  FOREIGN KEY (repo_name) REFERENCES repositories(name) ON DELETE CASCADE
);

-- Workspace-level settings (key-value store)
CREATE TABLE IF NOT EXISTS workspace_settings (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

-- =============================================================================
-- Indexes
-- =============================================================================

CREATE INDEX IF NOT EXISTS idx_worktrees_agent ON agent_worktrees(agent_name);
CREATE INDEX IF NOT EXISTS idx_worktrees_repo ON agent_worktrees(repo_name);
CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
CREATE INDEX IF NOT EXISTS idx_agents_theme ON agents(theme_id);
`;
/**
 * Ensure ephemeral agents are correctly typed based on their worktree path or naming pattern
 */
function ensureEphemeralAgentTypes(db) {
    // Check if agents table exists
    const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agents'").get();
    if (!tableExists) {
        return;
    }
    // Agents in temp directory should be ephemeral
    db.exec("UPDATE agents SET type = 'ephemeral' WHERE worktree_path LIKE 'agents/temp/%' AND type != 'ephemeral'");
    // Detect ephemeral agents by naming pattern: adjective-name-number (e.g., blue-khosla-1)
    // Staff agents are single names like 'lecun', 'musk', 'gates'
    db.exec(`
    UPDATE agents SET type = 'ephemeral'
    WHERE type != 'ephemeral'
    AND name GLOB '*-*-[0-9]*'
  `);
    // Also detect numberless ephemeral names (e.g., bold-bezos) using isEphemeralAgentName()
    // This catches agents that match the adjective-name pattern but don't have a number suffix
    const potentialEphemeral = db.prepare(`
    SELECT name FROM agents
    WHERE type != 'ephemeral'
    AND name LIKE '%-%'
    AND name NOT GLOB '*-*-[0-9]*'
  `).all();
    const updateStmt = db.prepare("UPDATE agents SET type = 'ephemeral' WHERE name = ?");
    for (const agent of potentialEphemeral) {
        if (isEphemeralAgentName(agent.name)) {
            updateStmt.run(agent.name);
        }
    }
}
/**
 * Get the database path for a workspace
 */
export function getDatabasePath(workspacePath) {
    return path.join(workspacePath, '.proletariat', 'workspace.db');
}
/**
 * Get the config path for a workspace
 */
export function getConfigPath(workspacePath) {
    return path.join(workspacePath, '.proletariat', 'config.json');
}
/**
 * Open workspace database connection
 */
export function openWorkspaceDatabase(workspacePath) {
    const dbPath = getDatabasePath(workspacePath);
    if (!fs.existsSync(dbPath)) {
        throw new Error(`Database not found: ${dbPath}. Run 'prlt init' first.`);
    }
    const db = new Database(dbPath);
    db.pragma('foreign_keys = ON');
    db.pragma('busy_timeout = 5000'); // Wait up to 5 seconds if database is locked
    // Ensure ephemeral agents are correctly typed
    ensureEphemeralAgentTypes(db);
    // Ensure theme tables exist
    db.exec(`
    CREATE TABLE IF NOT EXISTS agent_themes (
      id TEXT PRIMARY KEY,
      name TEXT NOT NULL UNIQUE,
      display_name TEXT NOT NULL,
      description TEXT,
      builtin BOOLEAN DEFAULT FALSE,
      created_at TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS agent_theme_names (
      theme_id TEXT NOT NULL,
      name TEXT NOT NULL,
      PRIMARY KEY (theme_id, name),
      FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
    );
    CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
    CREATE INDEX IF NOT EXISTS idx_agents_theme ON agents(theme_id);
  `);
    // Migration: drop 'used' column if it exists (no longer needed)
    try {
        const tableInfo = db.prepare("PRAGMA table_info(agent_theme_names)").all();
        if (tableInfo.some(col => col.name === 'used')) {
            // SQLite doesn't support DROP COLUMN directly, so recreate the table
            db.exec(`
        CREATE TABLE IF NOT EXISTS agent_theme_names_new (
          theme_id TEXT NOT NULL,
          name TEXT NOT NULL,
          PRIMARY KEY (theme_id, name),
          FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
        );
        INSERT OR IGNORE INTO agent_theme_names_new (theme_id, name)
          SELECT theme_id, name FROM agent_theme_names;
        DROP TABLE agent_theme_names;
        ALTER TABLE agent_theme_names_new RENAME TO agent_theme_names;
        CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
      `);
        }
    }
    catch {
        // Ignore migration errors - table might not exist yet
    }
    // Migration: add missing columns to agent_worktrees table (TKT-1014)
    try {
        const worktreesTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agent_worktrees'").get();
        if (worktreesTableExists) {
            const worktreeTableInfo = db.prepare("PRAGMA table_info(agent_worktrees)").all();
            if (!worktreeTableInfo.some(col => col.name === 'commits_ahead')) {
                db.exec("ALTER TABLE agent_worktrees ADD COLUMN last_commit_hash TEXT");
                db.exec("ALTER TABLE agent_worktrees ADD COLUMN commits_ahead INTEGER NOT NULL DEFAULT 0");
                db.exec("ALTER TABLE agent_worktrees ADD COLUMN is_clean INTEGER NOT NULL DEFAULT 1");
                db.exec("ALTER TABLE agent_worktrees ADD COLUMN last_checked TEXT");
            }
        }
    }
    catch {
        // Ignore migration errors - table might not exist yet or columns already exist
    }
    // Migration: add mount_mode column to agents table (TKT-686)
    try {
        const agentsTableInfo = db.prepare("PRAGMA table_info(agents)").all();
        if (!agentsTableInfo.some(col => col.name === 'mount_mode')) {
            // Add mount_mode column with default 'worktree' for existing agents (backward compat)
            db.exec("ALTER TABLE agents ADD COLUMN mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone'))");
        }
    }
    catch {
        // Ignore migration errors - table might not exist yet or column already exists
    }
    // Migration: add position column to pmo_tickets table (TKT-965)
    try {
        const ticketsTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pmo_tickets'").get();
        if (ticketsTableExists) {
            const ticketsTableInfo = db.prepare("PRAGMA table_info(pmo_tickets)").all();
            if (!ticketsTableInfo.some(col => col.name === 'position')) {
                db.exec("ALTER TABLE pmo_tickets ADD COLUMN position INTEGER NOT NULL DEFAULT 0");
                db.exec("CREATE INDEX IF NOT EXISTS idx_pmo_tickets_status_position ON pmo_tickets(status_id, position)");
                // Backfill existing tickets with gapped positions (1000, 2000, ...) per status,
                // ordered by priority then created_at
                const statuses = db.prepare("SELECT DISTINCT status_id FROM pmo_tickets WHERE status_id IS NOT NULL").all();
                const getTicketsForStatus = db.prepare(`
          SELECT id FROM pmo_tickets WHERE status_id = ?
          ORDER BY
            CASE priority
              WHEN 'P0' THEN 0
              WHEN 'P1' THEN 1
              WHEN 'P2' THEN 2
              WHEN 'P3' THEN 3
              ELSE 4
            END,
            created_at ASC
        `);
                const updatePosition = db.prepare("UPDATE pmo_tickets SET position = ? WHERE id = ?");
                const backfill = db.transaction(() => {
                    for (const { status_id } of statuses) {
                        const tickets = getTicketsForStatus.all(status_id);
                        tickets.forEach((ticket, idx) => {
                            updatePosition.run((idx + 1) * 1000, ticket.id);
                        });
                    }
                });
                backfill();
            }
        }
    }
    catch {
        // Ignore migration errors - table might not exist yet
    }
    return db;
}
/**
 * Create and initialize workspace database
 */
export function createWorkspaceDatabase(workspacePath, type, workspaceName, hasPMO = false) {
    const dbPath = getDatabasePath(workspacePath);
    const configPath = getConfigPath(workspacePath);
    // Ensure .proletariat directory exists
    const proletariatDir = path.dirname(dbPath);
    if (!fs.existsSync(proletariatDir)) {
        fs.mkdirSync(proletariatDir, { recursive: true });
    }
    // Create minimal config.json (bootstrap only)
    const bootstrapConfig = {
        version: "1.0.0",
        schemaVersion: 1
    };
    fs.writeFileSync(configPath, JSON.stringify(bootstrapConfig, null, 2));
    // Create and setup SQLite database
    const db = new Database(dbPath);
    // Enable foreign keys
    db.pragma('foreign_keys = ON');
    // Create core workspace tables (agents, repos, etc)
    db.exec(CREATE_TABLES_SQL);
    // Create PMO tables (from shared schema)
    db.exec(PMO_SCHEMA_SQL);
    // Insert workspace data (convert boolean to number for SQLite)
    db.prepare(`
    INSERT INTO workspace (id, type, workspace_name, has_pmo, created_at)
    VALUES (1, ?, ?, ?, ?)
  `).run(type, workspaceName, hasPMO ? 1 : 0, new Date().toISOString());
    return db;
}
/**
 * Get workspace configuration
 */
export function getWorkspaceConfig(workspacePath) {
    try {
        const db = openWorkspaceDatabase(workspacePath);
        const config = db.prepare('SELECT * FROM workspace LIMIT 1').get();
        db.close();
        return config || null;
    }
    catch {
        return null;
    }
}
/**
 * Get the active theme for a workspace
 * Auto-detects theme from existing agents if not explicitly set
 */
export function getActiveTheme(workspacePath) {
    const config = getWorkspaceConfig(workspacePath);
    // If explicitly set, use that
    if (config?.active_theme_id) {
        return getTheme(workspacePath, config.active_theme_id);
    }
    // Auto-detect from existing agents
    const agents = getWorkspaceAgents(workspacePath);
    if (agents.length === 0) {
        return null;
    }
    // Check if any agent has a theme_id set
    const themedAgent = agents.find(a => a.theme_id);
    if (themedAgent?.theme_id) {
        const theme = getTheme(workspacePath, themedAgent.theme_id);
        if (theme) {
            // Auto-set it for future use
            setActiveTheme(workspacePath, themedAgent.theme_id);
            return theme;
        }
    }
    // Check if agent names match any builtin theme
    const themes = getThemes(workspacePath);
    for (const theme of themes) {
        const themeNames = getThemeNames(workspacePath, theme.id);
        const themeNameSet = new Set(themeNames.map(n => n.name.toLowerCase()));
        // If any existing agent matches this theme's names
        const matchingAgent = agents.find(a => themeNameSet.has(a.name.toLowerCase()));
        if (matchingAgent) {
            // Auto-set it for future use
            setActiveTheme(workspacePath, theme.id);
            return theme;
        }
    }
    return null;
}
/**
 * Set the active theme for a workspace
 */
export function setActiveTheme(workspacePath, themeId) {
    const db = openWorkspaceDatabase(workspacePath);
    if (themeId) {
        // Validate theme exists
        const theme = db.prepare('SELECT id FROM agent_themes WHERE id = ?').get(themeId);
        if (!theme) {
            db.close();
            throw new Error(`Theme "${themeId}" not found`);
        }
    }
    db.prepare('UPDATE workspace SET active_theme_id = ? WHERE id = 1').run(themeId);
    db.close();
}
/**
 * Add repositories to database
 */
export function addRepositoriesToDatabase(workspacePath, repos) {
    const db = openWorkspaceDatabase(workspacePath);
    const insertRepo = db.prepare(`
    INSERT OR REPLACE INTO repositories (name, path, type, source_url, action, added_at)
    VALUES (?, ?, ?, ?, ?, ?)
  `);
    const transaction = db.transaction(() => {
        for (const repo of repos) {
            insertRepo.run(repo.name, repo.path, 'main', repo.source_url || null, repo.action || null, new Date().toISOString());
        }
    });
    transaction();
    db.close();
}
/**
 * Add agents to database (case-insensitive uniqueness)
 */
export function addAgentsToDatabase(workspacePath, agentNames, themeId, mountMode = 'worktree') {
    const db = openWorkspaceDatabase(workspacePath);
    // Check for existing agents (case-insensitive)
    const checkExisting = db.prepare('SELECT name FROM agents WHERE LOWER(name) = LOWER(?)');
    const insertAgent = db.prepare(`
    INSERT OR REPLACE INTO agents (name, type, base_name, theme_id, worktree_path, mount_mode, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `);
    const insertWorktree = db.prepare(`
    INSERT OR REPLACE INTO agent_worktrees (agent_name, repo_name, worktree_path, branch, created_at)
    VALUES (?, ?, ?, ?, ?)
  `);
    // Get workspace config to determine paths
    const workspace = db.prepare('SELECT * FROM workspace').get();
    // Get all repos for this workspace
    const repos = db.prepare('SELECT name FROM repositories').all();
    // Determine the effective theme ID (provided or active theme)
    const effectiveThemeId = themeId || workspace.active_theme_id || undefined;
    const persistentDir = getThemePersistentDir(effectiveThemeId);
    const transaction = db.transaction(() => {
        for (const agentName of agentNames) {
            // Skip if agent already exists (case-insensitive check)
            const existing = checkExisting.get(agentName);
            if (existing) {
                continue; // Agent already exists with same name (different case)
            }
            const now = new Date().toISOString();
            // Determine worktree path for the agent
            const agentWorktreePath = workspace.type === 'hq'
                ? `agents/${persistentDir}/${agentName}`
                : agentName;
            // Add agent (persistent type for manually added agents)
            insertAgent.run(agentName, 'persistent', null, effectiveThemeId || null, agentWorktreePath, mountMode, now);
            // Add worktrees for all repos
            for (const repo of repos) {
                const worktreePath = workspace.type === 'hq'
                    ? `agents/${persistentDir}/${agentName}/${repo.name}`
                    : `${agentName}/${repo.name}`;
                insertWorktree.run(agentName, repo.name, worktreePath, `agent-${agentName}`, now);
            }
        }
    });
    transaction();
    db.close();
}
/**
 * Add an ephemeral agent to the database.
 * Throws on name collision — use tryAddEphemeralAgentToDatabase for
 * concurrency-safe insertion with conflict detection.
 */
export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
    const result = tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode);
    if (!result) {
        throw new Error(`Agent name "${agentName}" already exists (UNIQUE constraint failed: agents.name)`);
    }
    return result;
}
/**
 * Try to add an ephemeral agent to the database.
 * Returns the Agent on success, or null if the name already exists
 * (SQLITE_CONSTRAINT_PRIMARYKEY). This is concurrency-safe: parallel
 * processes that generate the same name will not crash — the loser
 * simply gets null and can retry with a different name.
 */
export function tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
    const db = openWorkspaceDatabase(workspacePath);
    try {
        const now = new Date().toISOString();
        const worktreePath = `agents/temp/${agentName}`;
        db.prepare(`
      INSERT INTO agents (name, type, status, base_name, theme_id, worktree_path, mount_mode, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `).run(agentName, 'ephemeral', 'active', baseName, themeId || null, worktreePath, mountMode, now);
        const agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentName);
        return {
            name: agent.name,
            type: agent.type,
            status: agent.status,
            base_name: agent.base_name,
            theme_id: agent.theme_id,
            worktree_path: agent.worktree_path,
            mount_mode: (agent.mount_mode || 'clone'),
            created_at: agent.created_at,
            cleaned_at: agent.cleaned_at,
        };
    }
    catch (err) {
        const sqliteErr = err;
        if (sqliteErr.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' || sqliteErr.code === 'SQLITE_CONSTRAINT_UNIQUE') {
            return null;
        }
        throw err;
    }
    finally {
        db.close();
    }
}
/**
 * Get all ephemeral agent names from the database
 */
export function getEphemeralAgentNames(workspacePath) {
    const db = openWorkspaceDatabase(workspacePath);
    const agents = db.prepare("SELECT name FROM agents WHERE type = 'ephemeral'").all();
    db.close();
    return new Set(agents.map(a => a.name.toLowerCase()));
}
/**
 * Remove an ephemeral agent from the database
 */
export function removeEphemeralAgent(workspacePath, agentName) {
    const db = openWorkspaceDatabase(workspacePath);
    db.prepare("DELETE FROM agents WHERE name = ? AND type = 'ephemeral'").run(agentName);
    db.close();
}
/**
 * Get all agents in workspace
 */
export function getWorkspaceAgents(workspacePath, includeCleanedUp = false) {
    const db = openWorkspaceDatabase(workspacePath);
    const query = includeCleanedUp
        ? 'SELECT * FROM agents ORDER BY created_at'
        : "SELECT * FROM agents WHERE status = 'active' OR status IS NULL ORDER BY created_at";
    const rows = db.prepare(query).all();
    db.close();
    // Map rows to Agent type, handling missing columns in old databases
    return rows.map(row => ({
        name: row.name,
        type: (row.type || 'persistent'),
        status: (row.status || 'active'),
        base_name: row.base_name,
        theme_id: row.theme_id,
        worktree_path: row.worktree_path,
        mount_mode: (row.mount_mode || 'worktree'),
        created_at: row.created_at,
        cleaned_at: row.cleaned_at,
    }));
}
/**
 * Get an agent by directory path.
 * Looks up agent where the given absolute path is inside the agent's worktree.
 * Returns null if no matching agent found.
 */
export function getAgentByPath(workspacePath, absolutePath) {
    // Normalize paths
    const normalizedWorkspace = path.resolve(workspacePath);
    const normalizedPath = path.resolve(absolutePath);
    // Path must be inside workspace
    if (!normalizedPath.startsWith(normalizedWorkspace)) {
        return null;
    }
    // Get relative path from workspace root
    const relativePath = path.relative(normalizedWorkspace, normalizedPath);
    const db = openWorkspaceDatabase(workspacePath);
    const agents = db.prepare("SELECT * FROM agents WHERE status = 'active' OR status IS NULL").all();
    db.close();
    // Find agent whose worktree_path matches or contains the relative path
    for (const row of agents) {
        if (row.worktree_path) {
            // Check if relativePath starts with or equals the agent's worktree_path
            if (relativePath === row.worktree_path || relativePath.startsWith(row.worktree_path + '/')) {
                return {
                    name: row.name,
                    type: (row.type || 'persistent'),
                    status: (row.status || 'active'),
                    base_name: row.base_name,
                    theme_id: row.theme_id,
                    worktree_path: row.worktree_path,
                    mount_mode: (row.mount_mode || 'worktree'),
                    created_at: row.created_at,
                    cleaned_at: row.cleaned_at,
                };
            }
        }
    }
    return null;
}
/**
 * Mark an agent as cleaned up (keeps the record for history)
 */
export function markAgentCleaned(workspacePath, agentName) {
    const db = openWorkspaceDatabase(workspacePath);
    const now = new Date().toISOString();
    db.prepare("UPDATE agents SET status = 'cleaned', cleaned_at = ? WHERE name = ?").run(now, agentName);
    db.close();
}
/**
 * Sync agents in database with what exists on disk.
 * Marks agents as 'cleaned' if their directory no longer exists.
 * Returns list of agents that were cleaned up.
 */
export function syncAgentsWithDisk(workspacePath) {
    const agents = getWorkspaceAgents(workspacePath, false); // Only active agents
    const cleanedAgents = [];
    for (const agent of agents) {
        // Determine expected directory path
        let agentDir;
        if (agent.worktree_path) {
            agentDir = path.join(workspacePath, agent.worktree_path);
        }
        else if (agent.type === 'ephemeral') {
            agentDir = path.join(workspacePath, 'agents', 'temp', agent.name);
        }
        else {
            agentDir = path.join(workspacePath, 'agents', 'staff', agent.name);
        }
        // If directory doesn't exist, mark agent as cleaned
        if (!fs.existsSync(agentDir)) {
            markAgentCleaned(workspacePath, agent.name);
            cleanedAgents.push(agent.name);
        }
    }
    return cleanedAgents;
}
/**
 * Discover agents on disk that aren't in the database and register them.
 * Also cleans up agents in DB whose directories no longer exist.
 * Returns both discovered and cleaned agents.
 */
export function discoverAgentsOnDisk(workspacePath) {
    const result = { discovered: [], cleaned: [] };
    // First, clean up missing agents
    result.cleaned = syncAgentsWithDisk(workspacePath);
    // Get existing ACTIVE agents from DB (case-insensitive lookup)
    const activeAgents = getWorkspaceAgents(workspacePath, false); // Only active agents
    const activeNames = new Set(activeAgents.map(a => a.name.toLowerCase()));
    // Get ALL agents including cleaned (for reactivation)
    const allAgents = getWorkspaceAgents(workspacePath, true);
    const cleanedAgents = new Map(allAgents.filter(a => a.status === 'cleaned').map(a => [a.name.toLowerCase(), a]));
    const db = openWorkspaceDatabase(workspacePath);
    try {
        // Scan staff directory
        const staffDir = path.join(workspacePath, 'agents', 'staff');
        if (fs.existsSync(staffDir)) {
            const staffEntries = fs.readdirSync(staffDir, { withFileTypes: true });
            for (const entry of staffEntries) {
                if (entry.isDirectory() && !entry.name.startsWith('.')) {
                    const nameLower = entry.name.toLowerCase();
                    if (!activeNames.has(nameLower)) {
                        const worktreePath = `agents/staff/${entry.name}`;
                        const now = new Date().toISOString();
                        // Check if this is a cleaned agent that should be reactivated
                        const cleanedAgent = cleanedAgents.get(nameLower);
                        if (cleanedAgent) {
                            // Reactivate the cleaned agent
                            db.prepare(`
                UPDATE agents SET status = 'active', cleaned_at = NULL, worktree_path = ?
                WHERE LOWER(name) = LOWER(?)
              `).run(worktreePath, entry.name);
                        }
                        else {
                            // Register new agent - discovered agents default to 'worktree' mode (legacy behavior)
                            db.prepare(`
                INSERT INTO agents (name, type, status, worktree_path, mount_mode, created_at)
                VALUES (?, 'persistent', 'active', ?, 'worktree', ?)
              `).run(entry.name, worktreePath, now);
                        }
                        result.discovered.push({ name: entry.name, type: 'persistent', path: worktreePath });
                        activeNames.add(nameLower);
                    }
                }
            }
        }
        // Scan temp directory
        const tempDir = path.join(workspacePath, 'agents', 'temp');
        if (fs.existsSync(tempDir)) {
            const tempEntries = fs.readdirSync(tempDir, { withFileTypes: true });
            for (const entry of tempEntries) {
                if (entry.isDirectory() && !entry.name.startsWith('.')) {
                    const nameLower = entry.name.toLowerCase();
                    if (!activeNames.has(nameLower)) {
                        const worktreePath = `agents/temp/${entry.name}`;
                        const now = new Date().toISOString();
                        // Check if this is a cleaned agent that should be reactivated
                        const cleanedAgent = cleanedAgents.get(nameLower);
                        if (cleanedAgent) {
                            // Reactivate the cleaned agent
                            db.prepare(`
                UPDATE agents SET status = 'active', cleaned_at = NULL, worktree_path = ?
                WHERE LOWER(name) = LOWER(?)
              `).run(worktreePath, entry.name);
                        }
                        else {
                            // Register new agent - discovered agents default to 'worktree' mode (legacy behavior)
                            db.prepare(`
                INSERT INTO agents (name, type, status, worktree_path, mount_mode, created_at)
                VALUES (?, 'ephemeral', 'active', ?, 'worktree', ?)
              `).run(entry.name, worktreePath, now);
                        }
                        result.discovered.push({ name: entry.name, type: 'ephemeral', path: worktreePath });
                        activeNames.add(nameLower);
                    }
                }
            }
        }
    }
    finally {
        db.close();
    }
    return result;
}
/**
 * Get all repositories in workspace
 */
export function getWorkspaceRepositories(workspacePath) {
    const db = openWorkspaceDatabase(workspacePath);
    const repos = db.prepare('SELECT * FROM repositories ORDER BY added_at').all();
    db.close();
    return repos;
}
/**
 * Get worktrees for a specific agent
 */
export function getAgentWorktrees(workspacePath, agentName) {
    const db = openWorkspaceDatabase(workspacePath);
    const worktrees = db.prepare('SELECT * FROM agent_worktrees WHERE agent_name = ?').all(agentName);
    db.close();
    return worktrees;
}
/**
 * Find agent worktrees matching a branch pattern (case-insensitive LIKE).
 */
export function findWorktreesByBranch(workspacePath, branchPattern) {
    const db = openWorkspaceDatabase(workspacePath);
    const worktrees = db.prepare('SELECT * FROM agent_worktrees WHERE LOWER(branch) LIKE ?').all(branchPattern);
    db.close();
    return worktrees;
}
/**
 * Get agent worktrees for a specific repository.
 */
export function getWorktreesForRepo(workspacePath, repoName) {
    const db = openWorkspaceDatabase(workspacePath);
    const worktrees = db.prepare('SELECT agent_name, is_clean, commits_ahead, branch FROM agent_worktrees WHERE repo_name = ?').all(repoName);
    db.close();
    return worktrees;
}
/**
 * Upsert a workspace setting (key-value pair).
 */
export function upsertWorkspaceSetting(db, key, value) {
    db.prepare(`
    INSERT INTO workspace_settings (key, value)
    VALUES (?, ?)
    ON CONFLICT(key) DO UPDATE SET value = excluded.value
  `).run(key, value);
}
/**
 * Remove agents from database
 */
export function removeAgentsFromDatabase(workspacePath, agentNames) {
    const db = openWorkspaceDatabase(workspacePath);
    const deleteAgent = db.prepare('DELETE FROM agents WHERE name = ?');
    // Note: agent_worktrees will be deleted automatically due to CASCADE
    const transaction = db.transaction(() => {
        for (const agentName of agentNames) {
            deleteAgent.run(agentName);
        }
    });
    transaction();
    db.close();
}
// =============================================================================
// PMO Bootstrapping Operations
// =============================================================================
/**
 * Check if PMO tables exist and get basic stats.
 * Used by pmo init to detect existing PMO before storage layer is available.
 */
export function checkPMOExists(dbPath) {
    const db = new Database(dbPath);
    try {
        const result = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pmo_projects'").get();
        if (result === undefined) {
            return { exists: false, projectCount: 0, ticketCount: 0 };
        }
        const projectCountResult = db.prepare('SELECT COUNT(*) as count FROM pmo_projects').get();
        const ticketCountResult = db.prepare('SELECT COUNT(*) as count FROM pmo_tickets').get();
        return {
            exists: true,
            projectCount: projectCountResult.count,
            ticketCount: ticketCountResult.count,
        };
    }
    finally {
        db.close();
    }
}
/**
 * Get a PMO setting from the pmo_settings table.
 * Used for bootstrapping queries before storage layer is available.
 */
export function getPMOSetting(dbPath, key) {
    const db = new Database(dbPath);
    try {
        const result = db.prepare('SELECT value FROM pmo_settings WHERE key = ?').get(key);
        return result?.value ?? null;
    }
    catch {
        return null;
    }
    finally {
        db.close();
    }
}
/**
 * Drop PMO tables from the database.
 * Used during PMO reinitialization.
 */
export function dropPMOTables(dbPath, tables) {
    const db = new Database(dbPath);
    try {
        for (const table of tables) {
            try {
                db.prepare(`DROP TABLE IF EXISTS ${table}`).run();
            }
            catch {
                // Ignore errors - table might not exist
            }
        }
    }
    finally {
        db.close();
    }
}
// =============================================================================
// Theme CRUD Operations
// =============================================================================
/**
 * Get all themes
 */
export function getThemes(workspacePath) {
    const db = openWorkspaceDatabase(workspacePath);
    const themes = db.prepare('SELECT * FROM agent_themes ORDER BY builtin DESC, name').all();
    db.close();
    return themes;
}
/**
 * Get a theme by ID
 */
export function getTheme(workspacePath, themeId) {
    const db = openWorkspaceDatabase(workspacePath);
    const theme = db.prepare('SELECT * FROM agent_themes WHERE id = ?').get(themeId);
    db.close();
    return theme || null;
}
/**
 * Create a new theme
 */
export function createTheme(workspacePath, theme) {
    const db = openWorkspaceDatabase(workspacePath);
    const now = new Date().toISOString();
    db.prepare(`
    INSERT INTO agent_themes (id, name, display_name, description, builtin, created_at)
    VALUES (?, ?, ?, ?, ?, ?)
  `).run(theme.id, theme.name, theme.displayName, theme.description || null, theme.builtin ? 1 : 0, now);
    const created = db.prepare('SELECT * FROM agent_themes WHERE id = ?').get(theme.id);
    db.close();
    return created;
}
/**
 * Delete a theme (cannot delete builtin themes)
 */
export function deleteTheme(workspacePath, themeId) {
    const db = openWorkspaceDatabase(workspacePath);
    // Check if builtin
    const theme = db.prepare('SELECT builtin FROM agent_themes WHERE id = ?').get(themeId);
    if (!theme) {
        db.close();
        return false;
    }
    if (theme.builtin) {
        db.close();
        throw new Error('Cannot delete built-in themes');
    }
    db.prepare('DELETE FROM agent_themes WHERE id = ?').run(themeId);
    db.close();
    return true;
}
/**
 * Get names for a theme
 */
export function getThemeNames(workspacePath, themeId) {
    const db = openWorkspaceDatabase(workspacePath);
    const names = db.prepare('SELECT * FROM agent_theme_names WHERE theme_id = ? ORDER BY name').all(themeId);
    db.close();
    return names;
}
/**
 * Get available names for a theme.
 * A name is available if:
 * 1. No staff agent exists in the database with that name (case-insensitive), OR
 * 2. The agent exists but its worktree directory is missing (manually deleted)
 */
export function getAvailableThemeNames(workspacePath, themeId) {
    const db = openWorkspaceDatabase(workspacePath);
    // Get all theme names
    const names = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ? ORDER BY name').all(themeId);
    // Get existing staff agents with their worktree paths (persistent type only)
    const existingAgents = db.prepare(`
    SELECT LOWER(name) as name, worktree_path
    FROM agents
    WHERE type = 'persistent' AND (status = 'active' OR status IS NULL)
  `).all();
    db.close();
    // Build a set of names that are truly in use (agent exists AND worktree exists)
    const inUseNames = new Set();
    for (const agent of existingAgents) {
        if (agent.worktree_path) {
            const fullPath = path.join(workspacePath, agent.worktree_path);
            if (fs.existsSync(fullPath)) {
                inUseNames.add(agent.name);
            }
        }
        else {
            // No worktree path means we can't verify - treat as in use to be safe
            inUseNames.add(agent.name);
        }
    }
    // Filter out names that are truly in use
    return names
        .map(n => n.name)
        .filter(name => !inUseNames.has(name.toLowerCase()));
}
/**
 * Add names to a theme (case-insensitive uniqueness)
 */
export function addThemeNames(workspacePath, themeId, names) {
    const db = openWorkspaceDatabase(workspacePath);
    // Check for existing name (case-insensitive)
    const checkExisting = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ? AND LOWER(name) = LOWER(?)');
    const insertName = db.prepare(`
    INSERT INTO agent_theme_names (theme_id, name)
    VALUES (?, ?)
  `);
    const transaction = db.transaction(() => {
        for (const name of names) {
            // Skip if name already exists (case-insensitive)
            const existing = checkExisting.get(themeId, name);
            if (existing) {
                continue;
            }
            insertName.run(themeId, name);
        }
    });
    transaction();
    db.close();
}
//# sourceMappingURL=index.js.map