import { APP_CONFIG_TABLE_SCHEMA_SQL, LAST_SCANNED_AT_SQL, MESSAGE_BOOKMARKS_TABLE_SCHEMA_SQL, PROJECTS_TABLE_SCHEMA_SQL, PROMPT_TEMPLATES_TABLE_SCHEMA_SQL, SESSIONS_TABLE_SCHEMA_SQL, } from '../../modules/database/schema.js';
import { encrypt, hashApiKey, MIGRATION_SENTINEL, } from '../../modules/database/repositories/crypto-utils.js';
const SQLITE_UUID_SQL = `
lower(hex(randomblob(4))) || '-' ||
lower(hex(randomblob(2))) || '-' ||
lower(hex(randomblob(2))) || '-' ||
lower(hex(randomblob(2))) || '-' ||
lower(hex(randomblob(6)))
`;
const addColumnToTableIfNotExists = (db, tableName, columnNames, columnName, columnType) => {
    if (!columnNames.includes(columnName)) {
        console.log(`Running migration: Adding ${columnName} column to ${tableName} table`);
        db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`);
    }
};
const tableExists = (db, tableName) => Boolean(db
    .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
    .get(tableName));
const getTableInfo = (db, tableName) => db.prepare(`PRAGMA table_info(${tableName})`).all();
const migrateLegacySessionNames = (db) => {
    const hasLegacySessionNamesTable = tableExists(db, 'session_names');
    const hasSessionsTable = tableExists(db, 'sessions');
    if (!hasLegacySessionNamesTable) {
        return;
    }
    if (hasSessionsTable) {
        console.log('Running migration: Merging session_names into sessions');
        db.exec(`
      INSERT INTO sessions (session_id, provider, custom_name, created_at, updated_at)
      SELECT
        session_id,
        COALESCE(provider, 'claude'),
        custom_name,
        COALESCE(created_at, CURRENT_TIMESTAMP),
        COALESCE(updated_at, CURRENT_TIMESTAMP)
      FROM session_names
      WHERE true
      ON CONFLICT(session_id) DO UPDATE SET
        provider = excluded.provider,
        custom_name = COALESCE(excluded.custom_name, sessions.custom_name),
        created_at = COALESCE(sessions.created_at, excluded.created_at),
        updated_at = COALESCE(excluded.updated_at, sessions.updated_at)
    `);
        db.exec('DROP TABLE session_names');
        return;
    }
    console.log('Running migration: Renaming session_names table to sessions');
    db.exec('ALTER TABLE session_names RENAME TO sessions');
};
const migrateLegacyWorkspaceTableIntoProjects = (db) => {
    db.exec(PROJECTS_TABLE_SCHEMA_SQL);
    if (!tableExists(db, 'workspace_original_paths')) {
        return;
    }
    console.log('Running migration: Migrating workspace_original_paths data into projects');
    db.exec(`
    INSERT INTO projects (project_id, project_path, custom_project_name, isStarred, isArchived)
    SELECT
      CASE
        WHEN workspace_id IS NULL OR trim(workspace_id) = ''
        THEN ${SQLITE_UUID_SQL}
        ELSE workspace_id
      END,
      workspace_path,
      custom_workspace_name,
      COALESCE(isStarred, 0),
      0
    FROM workspace_original_paths
    WHERE workspace_path IS NOT NULL AND trim(workspace_path) <> ''
    ON CONFLICT(project_path) DO UPDATE SET
      custom_project_name = COALESCE(projects.custom_project_name, excluded.custom_project_name),
      isStarred = COALESCE(projects.isStarred, excluded.isStarred)
  `);
};
const rebuildProjectsTableWithPrimaryKeySchema = (db) => {
    const hasProjectsTable = tableExists(db, 'projects');
    if (!hasProjectsTable) {
        db.exec(PROJECTS_TABLE_SCHEMA_SQL);
        return;
    }
    const projectsTableInfo = getTableInfo(db, 'projects');
    const columnNames = projectsTableInfo.map((column) => column.name);
    const hasProjectIdPrimaryKey = projectsTableInfo.some((column) => column.name === 'project_id' && column.pk === 1);
    if (hasProjectIdPrimaryKey) {
        addColumnToTableIfNotExists(db, 'projects', columnNames, 'custom_project_name', 'TEXT DEFAULT NULL');
        addColumnToTableIfNotExists(db, 'projects', columnNames, 'isStarred', 'BOOLEAN DEFAULT 0');
        addColumnToTableIfNotExists(db, 'projects', columnNames, 'isArchived', 'BOOLEAN DEFAULT 0');
        db.exec(`
      UPDATE projects
      SET project_id = ${SQLITE_UUID_SQL}
      WHERE project_id IS NULL OR trim(project_id) = ''
    `);
        return;
    }
    console.log('Running migration: Rebuilding projects table to enforce project_id primary key');
    const projectPathExpression = columnNames.includes('project_path')
        ? 'project_path'
        : columnNames.includes('workspace_path')
            ? 'workspace_path'
            : 'NULL';
    const customProjectNameExpression = columnNames.includes('custom_project_name')
        ? 'custom_project_name'
        : columnNames.includes('custom_workspace_name')
            ? 'custom_workspace_name'
            : 'NULL';
    const isStarredExpression = columnNames.includes('isStarred') ? 'COALESCE(isStarred, 0)' : '0';
    const isArchivedExpression = columnNames.includes('isArchived') ? 'COALESCE(isArchived, 0)' : '0';
    const projectIdExpression = columnNames.includes('project_id')
        ? `CASE
         WHEN project_id IS NULL OR trim(project_id) = ''
         THEN ${SQLITE_UUID_SQL}
         ELSE project_id
       END`
        : SQLITE_UUID_SQL;
    db.exec('PRAGMA foreign_keys = OFF');
    try {
        db.exec('BEGIN TRANSACTION');
        db.exec('DROP TABLE IF EXISTS projects__new');
        db.exec(`
      CREATE TABLE projects__new (
        project_id TEXT PRIMARY KEY NOT NULL,
        project_path TEXT NOT NULL UNIQUE,
        custom_project_name TEXT DEFAULT NULL,
        isStarred BOOLEAN DEFAULT 0,
        isArchived BOOLEAN DEFAULT 0
      )
    `);
        db.exec(`
      WITH source_rows AS (
        SELECT
          ${projectPathExpression} AS project_path,
          ${customProjectNameExpression} AS custom_project_name,
          ${isStarredExpression} AS isStarred,
          ${isArchivedExpression} AS isArchived,
          ${projectIdExpression} AS candidate_project_id,
          rowid AS source_rowid
        FROM projects
        WHERE ${projectPathExpression} IS NOT NULL AND trim(${projectPathExpression}) <> ''
      ),
      deduped_paths AS (
        SELECT
          project_path,
          custom_project_name,
          isStarred,
          isArchived,
          candidate_project_id,
          source_rowid,
          ROW_NUMBER() OVER (PARTITION BY project_path ORDER BY source_rowid) AS project_path_rank
        FROM source_rows
      ),
      prepared_rows AS (
        SELECT
          CASE
            WHEN ROW_NUMBER() OVER (PARTITION BY candidate_project_id ORDER BY source_rowid) = 1
            THEN candidate_project_id
            ELSE ${SQLITE_UUID_SQL}
          END AS project_id,
          project_path,
          custom_project_name,
          isStarred,
          isArchived
        FROM deduped_paths
        WHERE project_path_rank = 1
      )
      INSERT INTO projects__new (
        project_id,
        project_path,
        custom_project_name,
        isStarred,
        isArchived
      )
      SELECT
        project_id,
        project_path,
        custom_project_name,
        isStarred,
        isArchived
      FROM prepared_rows
    `);
        db.exec('DROP TABLE projects');
        db.exec('ALTER TABLE projects__new RENAME TO projects');
        db.exec('COMMIT');
    }
    catch (migrationError) {
        db.exec('ROLLBACK');
        throw migrationError;
    }
    finally {
        db.exec('PRAGMA foreign_keys = ON');
    }
};
const rebuildSessionsTableWithProjectSchema = (db) => {
    const hasSessions = tableExists(db, 'sessions');
    if (!hasSessions) {
        db.exec(SESSIONS_TABLE_SCHEMA_SQL);
        return;
    }
    const sessionsTableInfo = getTableInfo(db, 'sessions');
    const columnNames = sessionsTableInfo.map((column) => column.name);
    const primaryKeyColumns = sessionsTableInfo
        .filter((column) => column.pk > 0)
        .sort((a, b) => a.pk - b.pk)
        .map((column) => column.name);
    const shouldRebuild = !columnNames.includes('project_path') ||
        primaryKeyColumns.length !== 1 ||
        primaryKeyColumns[0] !== 'session_id' ||
        !columnNames.includes('provider');
    if (!shouldRebuild) {
        addColumnToTableIfNotExists(db, 'sessions', columnNames, 'jsonl_path', 'TEXT');
        addColumnToTableIfNotExists(db, 'sessions', columnNames, 'isArchived', 'BOOLEAN DEFAULT 0');
        addColumnToTableIfNotExists(db, 'sessions', columnNames, 'created_at', 'DATETIME');
        addColumnToTableIfNotExists(db, 'sessions', columnNames, 'updated_at', 'DATETIME');
        db.exec('UPDATE sessions SET isArchived = COALESCE(isArchived, 0)');
        db.exec('UPDATE sessions SET created_at = COALESCE(created_at, CURRENT_TIMESTAMP)');
        db.exec('UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)');
        return;
    }
    console.log('Running migration: Rebuilding sessions table to project-based schema');
    const projectPathExpression = columnNames.includes('project_path')
        ? 'project_path'
        : columnNames.includes('workspace_path')
            ? 'workspace_path'
            : 'NULL';
    const providerExpression = columnNames.includes('provider')
        ? "COALESCE(provider, 'claude')"
        : "'claude'";
    const customNameExpression = columnNames.includes('custom_name')
        ? 'custom_name'
        : 'NULL';
    const jsonlPathExpression = columnNames.includes('jsonl_path')
        ? 'jsonl_path'
        : 'NULL';
    const isArchivedExpression = columnNames.includes('isArchived')
        ? 'COALESCE(isArchived, 0)'
        : '0';
    const createdAtExpression = columnNames.includes('created_at')
        ? 'COALESCE(created_at, CURRENT_TIMESTAMP)'
        : 'CURRENT_TIMESTAMP';
    const updatedAtExpression = columnNames.includes('updated_at')
        ? 'COALESCE(updated_at, CURRENT_TIMESTAMP)'
        : 'CURRENT_TIMESTAMP';
    db.exec('PRAGMA foreign_keys = OFF');
    try {
        db.exec('BEGIN TRANSACTION');
        db.exec('DROP TABLE IF EXISTS sessions__new');
        db.exec(`
      CREATE TABLE sessions__new (
        session_id TEXT NOT NULL,
        provider TEXT NOT NULL DEFAULT 'claude',
        custom_name TEXT,
        project_path TEXT,
        jsonl_path TEXT,
        isArchived BOOLEAN DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (session_id),
        FOREIGN KEY (project_path) REFERENCES projects(project_path)
        ON DELETE SET NULL
        ON UPDATE CASCADE
      )
    `);
        db.exec(`
      WITH source_rows AS (
        SELECT
          session_id,
          ${providerExpression} AS provider,
          ${customNameExpression} AS custom_name,
          ${projectPathExpression} AS project_path,
          ${jsonlPathExpression} AS jsonl_path,
          ${isArchivedExpression} AS isArchived,
          ${createdAtExpression} AS created_at,
          ${updatedAtExpression} AS updated_at,
          rowid AS source_rowid
        FROM sessions
        WHERE session_id IS NOT NULL AND trim(session_id) <> ''
      ),
      ranked_rows AS (
        SELECT
          session_id,
          provider,
          custom_name,
          project_path,
          jsonl_path,
          isArchived,
          created_at,
          updated_at,
          ROW_NUMBER() OVER (
            PARTITION BY session_id
            ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, source_rowid DESC
          ) AS session_rank
        FROM source_rows
      )
      INSERT INTO sessions__new (
        session_id,
        provider,
        custom_name,
        project_path,
        jsonl_path,
        isArchived,
        created_at,
        updated_at
      )
      SELECT
        session_id,
        provider,
        custom_name,
        project_path,
        jsonl_path,
        isArchived,
        created_at,
        updated_at
      FROM ranked_rows
      WHERE session_rank = 1
    `);
        db.exec('DROP TABLE sessions');
        db.exec('ALTER TABLE sessions__new RENAME TO sessions');
        db.exec('COMMIT');
    }
    catch (migrationError) {
        db.exec('ROLLBACK');
        throw migrationError;
    }
    finally {
        db.exec('PRAGMA foreign_keys = ON');
    }
};
const ensureProjectsForSessionPaths = (db) => {
    if (!tableExists(db, 'sessions')) {
        return;
    }
    db.exec(`
    INSERT INTO projects (project_id, project_path, custom_project_name, isStarred, isArchived)
    SELECT
      ${SQLITE_UUID_SQL},
      project_path,
      NULL,
      0,
      0
    FROM sessions
    WHERE project_path IS NOT NULL AND trim(project_path) <> ''
    ON CONFLICT(project_path) DO NOTHING
  `);
};
/** Detects whether a value is already in encrypted hex:hex:hex format. */
const isEncrypted = (value) => /^[0-9a-f]{32}:[0-9a-f]{32}:[0-9a-f]+$/i.test(value);
/** Detects whether an API key is already hashed (64 hex chars, no prefix). */
const isHashed = (value) => /^[0-9a-f]{64}$/i.test(value);
/**
 * Migrates existing plaintext secrets to hashed/encrypted form.
 *
 * Designed to run exactly once.  Uses a sentinel in app_config so that
 * subsequent boots, schema re-applies, and rollbacks are safe.
 */
const migratePlaintextSecrets = (db) => {
    const sentinel = db
        .prepare('SELECT value FROM app_config WHERE key = ?')
        .get(MIGRATION_SENTINEL);
    if (sentinel) {
        return;
    }
    console.log('Running migration: Hashing API keys and encrypting stored secrets');
    const apiKeysTableInfo = db.prepare('PRAGMA table_info(api_keys)').all();
    const apiKeyColumnNames = apiKeysTableInfo.map((c) => c.name);
    const migrateApiKeys = db.transaction(() => {
        addColumnToTableIfNotExists(db, 'api_keys', apiKeyColumnNames, 'api_key_prefix', 'TEXT NOT NULL DEFAULT \'\'');
        const rows = db
            .prepare('SELECT id, api_key FROM api_keys')
            .all();
        for (const row of rows) {
            if (isHashed(row.api_key))
                continue;
            const hash = hashApiKey(row.api_key);
            const prefix = row.api_key.substring(0, 10);
            db.prepare('UPDATE api_keys SET api_key = ?, api_key_prefix = ? WHERE id = ?').run(hash, prefix, row.id);
        }
    });
    const migrateCredentials = db.transaction(() => {
        const rows = db
            .prepare('SELECT id, credential_value FROM user_credentials')
            .all();
        for (const row of rows) {
            if (isEncrypted(row.credential_value))
                continue;
            const encrypted = encrypt(row.credential_value);
            db.prepare('UPDATE user_credentials SET credential_value = ? WHERE id = ?').run(encrypted, row.id);
        }
    });
    const runAll = db.transaction(() => {
        migrateApiKeys();
        migrateCredentials();
        db.prepare('INSERT OR REPLACE INTO app_config (key, value) VALUES (?, ?)').run(MIGRATION_SENTINEL, 'done');
    });
    runAll();
    console.log('Plaintext secret migration completed');
};
export const runMigrations = (db) => {
    try {
        const usersTableInfo = db.prepare('PRAGMA table_info(users)').all();
        const userColumnNames = usersTableInfo.map((column) => column.name);
        addColumnToTableIfNotExists(db, 'users', userColumnNames, 'git_name', 'TEXT');
        addColumnToTableIfNotExists(db, 'users', userColumnNames, 'git_email', 'TEXT');
        addColumnToTableIfNotExists(db, 'users', userColumnNames, 'has_completed_onboarding', 'BOOLEAN DEFAULT 0');
        db.exec(APP_CONFIG_TABLE_SCHEMA_SQL);
        db.exec(PROJECTS_TABLE_SCHEMA_SQL);
        rebuildProjectsTableWithPrimaryKeySchema(db);
        migrateLegacyWorkspaceTableIntoProjects(db);
        rebuildSessionsTableWithProjectSchema(db);
        migrateLegacySessionNames(db);
        ensureProjectsForSessionPaths(db);
        db.exec('CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)');
        db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path)');
        db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_is_archived ON sessions(isArchived)');
        db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_starred ON projects(isStarred)');
        db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_archived ON projects(isArchived)');
        db.exec('DROP INDEX IF EXISTS idx_session_names_lookup');
        db.exec('DROP INDEX IF EXISTS idx_sessions_workspace_path');
        db.exec('DROP INDEX IF EXISTS idx_workspace_original_paths_is_starred');
        db.exec('DROP INDEX IF EXISTS idx_workspace_original_paths_workspace_id');
        if (tableExists(db, 'workspace_original_paths')) {
            console.log('Running migration: Dropping legacy workspace_original_paths table');
            db.exec('DROP TABLE workspace_original_paths');
        }
        db.exec(LAST_SCANNED_AT_SQL);
        db.exec(PROMPT_TEMPLATES_TABLE_SCHEMA_SQL);
        db.exec('CREATE INDEX IF NOT EXISTS idx_prompt_templates_user_id ON prompt_templates(user_id)');
        db.exec('CREATE INDEX IF NOT EXISTS idx_prompt_templates_category ON prompt_templates(category)');
        db.exec(MESSAGE_BOOKMARKS_TABLE_SCHEMA_SQL);
        db.exec('CREATE INDEX IF NOT EXISTS idx_message_bookmarks_user_id ON message_bookmarks(user_id)');
        db.exec('CREATE INDEX IF NOT EXISTS idx_message_bookmarks_session_id ON message_bookmarks(session_id)');
        migratePlaintextSecrets(db);
        console.log('Database migrations completed successfully');
    }
    catch (error) {
        console.error('Error running migrations:', error.message);
        throw error;
    }
};
//# sourceMappingURL=migrations.js.map