import bcrypt from 'bcryptjs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from './logger.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const DATABASE_URL = process.env.DATABASE_URL;
const isPostgres = !!(DATABASE_URL && DATABASE_URL.startsWith('postgres'));

let db;

// Convert ? placeholders to $1, $2, ... for PostgreSQL
function convertPlaceholders(sql) {
  let index = 0;
  return sql.replace(/\?/g, () => `$${++index}`);
}

// ── SQLite Adapter ──────────────────────────────────────────────────────────
class SqliteAdapter {
  async init() {
    const { default: Database } = await import('better-sqlite3');
    const dbPath = process.env.SQLITE_PATH || path.join(__dirname, '..', '..', 'redlight.db');
    this.db = Database(dbPath);
    this.db.pragma('journal_mode = WAL');
    this.db.pragma('foreign_keys = ON');
  }

  async get(sql, params = []) {
    return this.db.prepare(sql).get(...params);
  }

  async all(sql, params = []) {
    return this.db.prepare(sql).all(...params);
  }

  async run(sql, params = []) {
    const result = this.db.prepare(sql).run(...params);
    return { lastInsertRowid: Number(result.lastInsertRowid), changes: result.changes };
  }

  async exec(sql) {
    this.db.exec(sql);
  }

  async columnExists(table, column) {
    const columns = this.db.pragma(`table_info(${table})`);
    return !!columns.find(c => c.name === column);
  }

  async columnIsNullable(table, column) {
    const columns = this.db.pragma(`table_info(${table})`);
    const col = columns.find(c => c.name === column);
    return col ? col.notnull === 0 : true;
  }

  close() {
    this.db.close();
  }
}

// ── PostgreSQL Adapter ──────────────────────────────────────────────────────
class PostgresAdapter {
  async init() {
    const pg = await import('pg');
    // Parse int8 (bigint / COUNT) as JS number instead of string
    pg.default.types.setTypeParser(20, val => parseInt(val, 10));
    this.pool = new pg.default.Pool({ connectionString: DATABASE_URL });
  }

  async get(sql, params = []) {
    const result = await this.pool.query(convertPlaceholders(sql), params);
    return result.rows[0] || undefined;
  }

  async all(sql, params = []) {
    const result = await this.pool.query(convertPlaceholders(sql), params);
    return result.rows;
  }

  async run(sql, params = []) {
    let pgSql = convertPlaceholders(sql);
    const isInsert = /^\s*INSERT/i.test(pgSql);
    if (isInsert && !/RETURNING/i.test(pgSql)) {
      // Some tables (e.g. settings, oauth_states) have no "id" column.
      // Return the inserted row generically and read id only when present.
      pgSql += ' RETURNING *';
    }
    const result = await this.pool.query(pgSql, params);
    return {
      lastInsertRowid: isInsert ? result.rows[0]?.id : undefined,
      changes: result.rowCount,
    };
  }

  async exec(sql) {
    await this.pool.query(sql);
  }

  async columnExists(table, column) {
    const result = await this.pool.query(
      'SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = $2',
      [table, column]
    );
    return result.rows.length > 0;
  }

  async columnIsNullable(table, column) {
    const result = await this.pool.query(
      'SELECT is_nullable FROM information_schema.columns WHERE table_name = $1 AND column_name = $2',
      [table, column]
    );
    return result.rows.length > 0 ? result.rows[0].is_nullable === 'YES' : true;
  }

  close() {
    this.pool?.end();
  }
}

// ── Public API ──────────────────────────────────────────────────────────────
export function getDb() {
  if (!db) {
    throw new Error('Database not initialised - call initDatabase() first');
  }
  return db;
}

export async function initDatabase() {
  // Create the right adapter
  if (isPostgres) {
    log.db.info('Using PostgreSQL database');
    db = new PostgresAdapter();
  } else {
    log.db.info('Using SQLite database');
    db = new SqliteAdapter();
  }
  await db.init();

  // ── Schema creation ─────────────────────────────────────────────────────
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name TEXT NOT NULL,
        display_name TEXT DEFAULT '',
        email TEXT UNIQUE NOT NULL,
        password_hash TEXT NOT NULL,
        role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')),
        language TEXT DEFAULT 'de',
        theme TEXT DEFAULT 'dark',
        avatar_color TEXT DEFAULT '#6366f1',
        avatar_image TEXT DEFAULT NULL,
        email_verified INTEGER DEFAULT 0,
        verification_token TEXT,
        verification_token_expires TIMESTAMP,
        verification_resend_at TIMESTAMP DEFAULT NULL,
        created_at TIMESTAMP DEFAULT NOW(),
        updated_at TIMESTAMP DEFAULT NOW()
      );

      CREATE TABLE IF NOT EXISTS rooms (
        id SERIAL PRIMARY KEY,
        uid TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        welcome_message TEXT DEFAULT 'Willkommen im Meeting!',
        max_participants INTEGER DEFAULT 0,
        access_code TEXT,
        mute_on_join INTEGER DEFAULT 1,
        require_approval INTEGER DEFAULT 0,
        anyone_can_start INTEGER DEFAULT 0,
        all_join_moderator INTEGER DEFAULT 0,
        record_meeting INTEGER DEFAULT 1,
        guest_access INTEGER DEFAULT 0,
        moderator_code TEXT,
        presentation_file TEXT DEFAULT NULL,
        created_at TIMESTAMP DEFAULT NOW(),
        updated_at TIMESTAMP DEFAULT NOW()
      );

      CREATE TABLE IF NOT EXISTS room_shares (
        id SERIAL PRIMARY KEY,
        room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        created_at TIMESTAMP DEFAULT NOW(),
        UNIQUE(room_id, user_id)
      );

      CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id);
      CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid);
      CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
      CREATE INDEX IF NOT EXISTS idx_room_shares_room_id ON room_shares(room_id);
      CREATE INDEX IF NOT EXISTS idx_room_shares_user_id ON room_shares(user_id);

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

      CREATE TABLE IF NOT EXISTS federation_invitations (
        id SERIAL PRIMARY KEY,
        invite_id TEXT UNIQUE NOT NULL,
        from_user TEXT NOT NULL,
        to_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        room_name TEXT NOT NULL,
        message TEXT,
        join_url TEXT NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
        created_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_fed_inv_to_user ON federation_invitations(to_user_id);
      CREATE INDEX IF NOT EXISTS idx_fed_inv_invite_id ON federation_invitations(invite_id);

      CREATE TABLE IF NOT EXISTS federated_rooms (
        id SERIAL PRIMARY KEY,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        invite_id TEXT UNIQUE NOT NULL,
        room_name TEXT NOT NULL,
        from_user TEXT NOT NULL,
        join_url TEXT NOT NULL,
        meet_id TEXT,
        max_participants INTEGER DEFAULT 0,
        allow_recording INTEGER DEFAULT 1,
        created_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_fed_rooms_user_id ON federated_rooms(user_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        display_name TEXT DEFAULT '',
        email TEXT UNIQUE NOT NULL,
        password_hash TEXT NOT NULL,
        role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')),
        language TEXT DEFAULT 'de',
        theme TEXT DEFAULT 'dark',
        avatar_color TEXT DEFAULT '#6366f1',
        avatar_image TEXT DEFAULT NULL,
        email_verified INTEGER DEFAULT 0,
        verification_token TEXT,
        verification_token_expires DATETIME,
        verification_resend_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
      );

      CREATE TABLE IF NOT EXISTS rooms (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        uid TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,
        user_id INTEGER NOT NULL,
        welcome_message TEXT DEFAULT 'Willkommen im Meeting!',
        max_participants INTEGER DEFAULT 0,
        access_code TEXT,
        mute_on_join INTEGER DEFAULT 1,
        require_approval INTEGER DEFAULT 0,
        anyone_can_start INTEGER DEFAULT 0,
        all_join_moderator INTEGER DEFAULT 0,
        record_meeting INTEGER DEFAULT 1,
        guest_access INTEGER DEFAULT 0,
        moderator_code TEXT,
        presentation_file TEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );

      CREATE TABLE IF NOT EXISTS room_shares (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        room_id INTEGER NOT NULL,
        user_id INTEGER NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(room_id, user_id),
        FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );

      CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id);
      CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid);
      CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
      CREATE INDEX IF NOT EXISTS idx_room_shares_room_id ON room_shares(room_id);
      CREATE INDEX IF NOT EXISTS idx_room_shares_user_id ON room_shares(user_id);

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

      CREATE TABLE IF NOT EXISTS federation_invitations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        invite_id TEXT UNIQUE NOT NULL,
        from_user TEXT NOT NULL,
        to_user_id INTEGER NOT NULL,
        room_name TEXT NOT NULL,
        message TEXT,
        join_url TEXT NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE
      );

      CREATE INDEX IF NOT EXISTS idx_fed_inv_to_user ON federation_invitations(to_user_id);
      CREATE INDEX IF NOT EXISTS idx_fed_inv_invite_id ON federation_invitations(invite_id);

      CREATE TABLE IF NOT EXISTS federated_rooms (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        invite_id TEXT UNIQUE NOT NULL,
        room_name TEXT NOT NULL,
        from_user TEXT NOT NULL,
        join_url TEXT NOT NULL,
        meet_id TEXT,
        max_participants INTEGER DEFAULT 0,
        allow_recording INTEGER DEFAULT 1,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_fed_rooms_user_id ON federated_rooms(user_id);
    `);
  }

  // ── Migrations ──────────────────────────────────────────────────────────
  if (!(await db.columnExists('users', 'avatar_image'))) {
    await db.exec('ALTER TABLE users ADD COLUMN avatar_image TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('rooms', 'guest_access'))) {
    await db.exec('ALTER TABLE rooms ADD COLUMN guest_access INTEGER DEFAULT 0');
  }
  if (!(await db.columnExists('rooms', 'moderator_code'))) {
    await db.exec('ALTER TABLE rooms ADD COLUMN moderator_code TEXT');
  }
  if (!(await db.columnExists('users', 'email_verified'))) {
    await db.exec('ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0');
  }
  if (!(await db.columnExists('users', 'verification_token'))) {
    await db.exec('ALTER TABLE users ADD COLUMN verification_token TEXT');
  }
  if (!(await db.columnExists('users', 'verification_token_expires'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE users ADD COLUMN verification_token_expires TIMESTAMP');
    } else {
      await db.exec('ALTER TABLE users ADD COLUMN verification_token_expires DATETIME');
    }
  }
  if (!(await db.columnExists('federated_rooms', 'meet_id'))) {
    await db.exec('ALTER TABLE federated_rooms ADD COLUMN meet_id TEXT');
  }
  if (!(await db.columnExists('federated_rooms', 'max_participants'))) {
    await db.exec('ALTER TABLE federated_rooms ADD COLUMN max_participants INTEGER DEFAULT 0');
  }
  if (!(await db.columnExists('federated_rooms', 'allow_recording'))) {
    await db.exec('ALTER TABLE federated_rooms ADD COLUMN allow_recording INTEGER DEFAULT 1');
  }
  if (!(await db.columnExists('federation_invitations', 'room_uid'))) {
    await db.exec('ALTER TABLE federation_invitations ADD COLUMN room_uid TEXT');
  }
  if (!(await db.columnExists('federation_invitations', 'max_participants'))) {
    await db.exec('ALTER TABLE federation_invitations ADD COLUMN max_participants INTEGER DEFAULT 0');
  }
  if (!(await db.columnExists('federation_invitations', 'allow_recording'))) {
    await db.exec('ALTER TABLE federation_invitations ADD COLUMN allow_recording INTEGER DEFAULT 1');
  }
  if (!(await db.columnExists('users', 'display_name'))) {
    await db.exec("ALTER TABLE users ADD COLUMN display_name TEXT DEFAULT ''");
    await db.exec("UPDATE users SET display_name = name WHERE display_name = ''");
  }
  if (!(await db.columnExists('rooms', 'presentation_file'))) {
    await db.exec('ALTER TABLE rooms ADD COLUMN presentation_file TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('rooms', 'presentation_name'))) {
    await db.exec('ALTER TABLE rooms ADD COLUMN presentation_name TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('users', 'verification_resend_at'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE users ADD COLUMN verification_resend_at TIMESTAMP DEFAULT NULL');
    } else {
      await db.exec('ALTER TABLE users ADD COLUMN verification_resend_at DATETIME DEFAULT NULL');
    }
  }

  // Password reset: store a SHA-256 hash of the reset token (never the raw token)
  if (!(await db.columnExists('users', 'reset_token_hash'))) {
    await db.exec('ALTER TABLE users ADD COLUMN reset_token_hash TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('users', 'reset_token_expires'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE users ADD COLUMN reset_token_expires TIMESTAMP DEFAULT NULL');
    } else {
      await db.exec('ALTER TABLE users ADD COLUMN reset_token_expires DATETIME DEFAULT NULL');
    }
  }
  if (!(await db.columnExists('users', 'reset_requested_at'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE users ADD COLUMN reset_requested_at TIMESTAMP DEFAULT NULL');
    } else {
      await db.exec('ALTER TABLE users ADD COLUMN reset_requested_at DATETIME DEFAULT NULL');
    }
  }

  // Federation sync: add deleted + updated_at to federated_rooms
  if (!(await db.columnExists('federated_rooms', 'deleted'))) {
    await db.exec('ALTER TABLE federated_rooms ADD COLUMN deleted INTEGER DEFAULT 0');
  }
  if (!(await db.columnExists('federated_rooms', 'updated_at'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE federated_rooms ADD COLUMN updated_at TIMESTAMP DEFAULT NOW()');
    } else {
      await db.exec('ALTER TABLE federated_rooms ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP');
    }
  }

  // Track outbound federation invites for deletion propagation
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS federation_outbound_invites (
        id SERIAL PRIMARY KEY,
        room_uid TEXT NOT NULL,
        remote_domain TEXT NOT NULL,
        created_at TIMESTAMP DEFAULT NOW(),
        UNIQUE(room_uid, remote_domain)
      );
      CREATE INDEX IF NOT EXISTS idx_fed_out_room_uid ON federation_outbound_invites(room_uid);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS federation_outbound_invites (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        room_uid TEXT NOT NULL,
        remote_domain TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(room_uid, remote_domain)
      );
      CREATE INDEX IF NOT EXISTS idx_fed_out_room_uid ON federation_outbound_invites(room_uid);
    `);
  }

  // User invite tokens (invite-only registration)
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS user_invites (
        id SERIAL PRIMARY KEY,
        token TEXT UNIQUE NOT NULL,
        email TEXT NOT NULL,
        created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        used_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
        used_at TIMESTAMP,
        expires_at TIMESTAMP NOT NULL,
        created_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_user_invites_token ON user_invites(token);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS user_invites (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        token TEXT UNIQUE NOT NULL,
        email TEXT NOT NULL,
        created_by INTEGER NOT NULL,
        used_by INTEGER,
        used_at DATETIME,
        expires_at DATETIME NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE,
        FOREIGN KEY (used_by) REFERENCES users(id) ON DELETE SET NULL
      );
      CREATE INDEX IF NOT EXISTS idx_user_invites_token ON user_invites(token);
    `);
  }

  // ── Calendar tables ──────────────────────────────────────────────────────
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_events (
        id SERIAL PRIMARY KEY,
        uid TEXT UNIQUE NOT NULL,
        title TEXT NOT NULL,
        description TEXT,
        start_time TIMESTAMP NOT NULL,
        end_time TIMESTAMP NOT NULL,
        room_uid TEXT,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        color TEXT DEFAULT '#6366f1',
        federated_from TEXT DEFAULT NULL,
        federated_join_url TEXT DEFAULT NULL,
        created_at TIMESTAMP DEFAULT NOW(),
        updated_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_cal_events_user_id ON calendar_events(user_id);
      CREATE INDEX IF NOT EXISTS idx_cal_events_uid ON calendar_events(uid);
      CREATE INDEX IF NOT EXISTS idx_cal_events_start ON calendar_events(start_time);

      CREATE TABLE IF NOT EXISTS calendar_event_shares (
        id SERIAL PRIMARY KEY,
        event_id INTEGER NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        created_at TIMESTAMP DEFAULT NOW(),
        UNIQUE(event_id, user_id)
      );
      CREATE INDEX IF NOT EXISTS idx_cal_shares_event ON calendar_event_shares(event_id);
      CREATE INDEX IF NOT EXISTS idx_cal_shares_user ON calendar_event_shares(user_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_events (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        uid TEXT UNIQUE NOT NULL,
        title TEXT NOT NULL,
        description TEXT,
        start_time DATETIME NOT NULL,
        end_time DATETIME NOT NULL,
        room_uid TEXT,
        user_id INTEGER NOT NULL,
        color TEXT DEFAULT '#6366f1',
        federated_from TEXT DEFAULT NULL,
        federated_join_url TEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_cal_events_user_id ON calendar_events(user_id);
      CREATE INDEX IF NOT EXISTS idx_cal_events_uid ON calendar_events(uid);
      CREATE INDEX IF NOT EXISTS idx_cal_events_start ON calendar_events(start_time);

      CREATE TABLE IF NOT EXISTS calendar_event_shares (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        event_id INTEGER NOT NULL,
        user_id INTEGER NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(event_id, user_id),
        FOREIGN KEY (event_id) REFERENCES calendar_events(id) ON DELETE CASCADE,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_cal_shares_event ON calendar_event_shares(event_id);
      CREATE INDEX IF NOT EXISTS idx_cal_shares_user ON calendar_event_shares(user_id);
    `);
  }

  // Calendar migrations: add federated columns if missing
  if (!(await db.columnExists('calendar_events', 'federated_from'))) {
    await db.exec('ALTER TABLE calendar_events ADD COLUMN federated_from TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('calendar_events', 'federated_join_url'))) {
    await db.exec('ALTER TABLE calendar_events ADD COLUMN federated_join_url TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('calendar_events', 'reminder_minutes'))) {
    await db.exec('ALTER TABLE calendar_events ADD COLUMN reminder_minutes INTEGER DEFAULT NULL');
  }
  if (!(await db.columnExists('calendar_events', 'reminder_sent_at'))) {
    await db.exec(`ALTER TABLE calendar_events ADD COLUMN reminder_sent_at ${isPostgres ? 'TIMESTAMP' : 'DATETIME'} DEFAULT NULL`);
  }

  // Calendar invitations (federated calendar events that must be accepted first)
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_invitations (
        id SERIAL PRIMARY KEY,
        event_uid TEXT NOT NULL,
        title TEXT NOT NULL,
        description TEXT,
        start_time TIMESTAMP NOT NULL,
        end_time TIMESTAMP NOT NULL,
        room_uid TEXT,
        join_url TEXT,
        from_user TEXT NOT NULL,
        to_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        color TEXT DEFAULT '#6366f1',
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
        created_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_cal_inv_to_user ON calendar_invitations(to_user_id);
      CREATE UNIQUE INDEX IF NOT EXISTS idx_cal_inv_uid_user ON calendar_invitations(event_uid, to_user_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_invitations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        event_uid TEXT NOT NULL,
        title TEXT NOT NULL,
        description TEXT,
        start_time DATETIME NOT NULL,
        end_time DATETIME NOT NULL,
        room_uid TEXT,
        join_url TEXT,
        from_user TEXT NOT NULL,
        to_user_id INTEGER NOT NULL,
        color TEXT DEFAULT '#6366f1',
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE,
        UNIQUE(event_uid, to_user_id)
      );
      CREATE INDEX IF NOT EXISTS idx_cal_inv_to_user ON calendar_invitations(to_user_id);
    `);
  }

  // Track outbound calendar event federation sends for deletion propagation
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_event_outbound (
        id SERIAL PRIMARY KEY,
        event_uid TEXT NOT NULL,
        remote_domain TEXT NOT NULL,
        created_at TIMESTAMP DEFAULT NOW(),
        UNIQUE(event_uid, remote_domain)
      );
      CREATE INDEX IF NOT EXISTS idx_cal_out_event_uid ON calendar_event_outbound(event_uid);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_event_outbound (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        event_uid TEXT NOT NULL,
        remote_domain TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(event_uid, remote_domain)
      );
      CREATE INDEX IF NOT EXISTS idx_cal_out_event_uid ON calendar_event_outbound(event_uid);
    `);
  }

  // Local calendar event invitations (share-with-acceptance flow)
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_local_invitations (
        id SERIAL PRIMARY KEY,
        event_id INTEGER NOT NULL REFERENCES calendar_events(id) ON DELETE CASCADE,
        from_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        to_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
        created_at TIMESTAMP DEFAULT NOW(),
        UNIQUE(event_id, to_user_id)
      );
      CREATE INDEX IF NOT EXISTS idx_cal_local_inv_to_user ON calendar_local_invitations(to_user_id);
      CREATE INDEX IF NOT EXISTS idx_cal_local_inv_event ON calendar_local_invitations(event_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS calendar_local_invitations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        event_id INTEGER NOT NULL,
        from_user_id INTEGER NOT NULL,
        to_user_id INTEGER NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(status IN ('pending','accepted','declined')),
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(event_id, to_user_id),
        FOREIGN KEY (event_id) REFERENCES calendar_events(id) ON DELETE CASCADE,
        FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE,
        FOREIGN KEY (to_user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_cal_local_inv_to_user ON calendar_local_invitations(to_user_id);
      CREATE INDEX IF NOT EXISTS idx_cal_local_inv_event ON calendar_local_invitations(event_id);
    `);
  }

  // ── Notifications table ──────────────────────────────────────────────────
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS notifications (
        id SERIAL PRIMARY KEY,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        type TEXT NOT NULL,
        title TEXT NOT NULL,
        body TEXT,
        link TEXT,
        read INTEGER DEFAULT 0,
        created_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS notifications (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        type TEXT NOT NULL,
        title TEXT NOT NULL,
        body TEXT,
        link TEXT,
        read INTEGER DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
    `);
  }

  // ── CalDAV tokens ────────────────────────────────────────────────────────
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS caldav_tokens (
        id SERIAL PRIMARY KEY,
        user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        token TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,
        created_at TIMESTAMP DEFAULT NOW(),
        last_used_at TIMESTAMP DEFAULT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_caldav_tokens_user_id ON caldav_tokens(user_id);
      CREATE INDEX IF NOT EXISTS idx_caldav_tokens_token ON caldav_tokens(token);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS caldav_tokens (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        token TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        last_used_at DATETIME DEFAULT NULL,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_caldav_tokens_user_id ON caldav_tokens(user_id);
      CREATE INDEX IF NOT EXISTS idx_caldav_tokens_token ON caldav_tokens(token);
    `);
  }

  // CalDAV: add token_hash column for SHA-256 hashed token lookup
  if (!(await db.columnExists('caldav_tokens', 'token_hash'))) {
    await db.exec('ALTER TABLE caldav_tokens ADD COLUMN token_hash TEXT DEFAULT NULL');
    await db.exec('CREATE INDEX IF NOT EXISTS idx_caldav_tokens_hash ON caldav_tokens(token_hash)');
  }

  // CalDAV: make token column nullable (now only token_hash is stored for new tokens)
  if (!(await db.columnIsNullable('caldav_tokens', 'token'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE caldav_tokens ALTER COLUMN token DROP NOT NULL');
    } else {
      // SQLite does not support ALTER COLUMN — recreate the table
      await db.exec(`
        CREATE TABLE caldav_tokens_new (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          token TEXT UNIQUE,
          token_hash TEXT DEFAULT NULL,
          name TEXT NOT NULL,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          last_used_at DATETIME DEFAULT NULL,
          FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
        );
        INSERT INTO caldav_tokens_new (id, user_id, token, token_hash, name, created_at, last_used_at)
          SELECT id, user_id, token, token_hash, name, created_at, last_used_at FROM caldav_tokens;
        DROP TABLE caldav_tokens;
        ALTER TABLE caldav_tokens_new RENAME TO caldav_tokens;
        CREATE INDEX IF NOT EXISTS idx_caldav_tokens_user_id ON caldav_tokens(user_id);
        CREATE INDEX IF NOT EXISTS idx_caldav_tokens_token ON caldav_tokens(token);
        CREATE INDEX IF NOT EXISTS idx_caldav_tokens_hash ON caldav_tokens(token_hash);
      `);
    }
  }

  // ── OAuth tables ────────────────────────────────────────────────────────
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS oauth_states (
        state TEXT PRIMARY KEY,
        provider TEXT NOT NULL,
        code_verifier TEXT NOT NULL,
        return_to TEXT,
        expires_at TIMESTAMP NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS oauth_states (
        state TEXT PRIMARY KEY,
        provider TEXT NOT NULL,
        code_verifier TEXT NOT NULL,
        return_to TEXT,
        expires_at DATETIME NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at);
    `);
  }

  // Add OAuth columns to users table
  if (!(await db.columnExists('users', 'oauth_provider'))) {
    await db.exec('ALTER TABLE users ADD COLUMN oauth_provider TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('users', 'oauth_provider_id'))) {
    await db.exec('ALTER TABLE users ADD COLUMN oauth_provider_id TEXT DEFAULT NULL');
  }

  // ── Learning Analytics table ─────────────────────────────────────────────
  if (!(await db.columnExists('rooms', 'learning_analytics'))) {
    await db.exec('ALTER TABLE rooms ADD COLUMN learning_analytics INTEGER DEFAULT 0');
  }

  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS learning_analytics_data (
        id SERIAL PRIMARY KEY,
        room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
        meeting_id TEXT NOT NULL,
        meeting_name TEXT,
        data JSONB NOT NULL,
        created_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_analytics_room_id ON learning_analytics_data(room_id);
      CREATE INDEX IF NOT EXISTS idx_analytics_meeting_id ON learning_analytics_data(meeting_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS learning_analytics_data (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        room_id INTEGER NOT NULL,
        meeting_id TEXT NOT NULL,
        meeting_name TEXT,
        data TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
      );
      CREATE INDEX IF NOT EXISTS idx_analytics_room_id ON learning_analytics_data(room_id);
      CREATE INDEX IF NOT EXISTS idx_analytics_meeting_id ON learning_analytics_data(meeting_id);
    `);
  }

  // ── TOTP 2FA columns ──────────────────────────────────────────────────────
  if (!(await db.columnExists('users', 'totp_secret'))) {
    await db.exec('ALTER TABLE users ADD COLUMN totp_secret TEXT DEFAULT NULL');
  }
  if (!(await db.columnExists('users', 'totp_enabled'))) {
    await db.exec('ALTER TABLE users ADD COLUMN totp_enabled INTEGER DEFAULT 0');
  }

  // ── Analytics visibility setting ────────────────────────────────────────
  if (!(await db.columnExists('rooms', 'analytics_visibility'))) {
    await db.exec("ALTER TABLE rooms ADD COLUMN analytics_visibility TEXT DEFAULT 'owner'");
  }

  // ── Recordings cache table ────────────────────────────────────────────
  if (isPostgres) {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS recordings (
        id SERIAL PRIMARY KEY,
        record_id TEXT UNIQUE NOT NULL,
        meeting_id TEXT NOT NULL,
        name TEXT,
        state TEXT,
        published INTEGER DEFAULT 1,
        start_time TEXT,
        end_time TEXT,
        participants TEXT,
        size TEXT,
        formats TEXT NOT NULL DEFAULT '[]',
        metadata TEXT DEFAULT '{}',
        created_at TIMESTAMP DEFAULT NOW(),
        updated_at TIMESTAMP DEFAULT NOW()
      );
      CREATE INDEX IF NOT EXISTS idx_recordings_record_id ON recordings(record_id);
      CREATE INDEX IF NOT EXISTS idx_recordings_meeting_id ON recordings(meeting_id);
    `);
  } else {
    await db.exec(`
      CREATE TABLE IF NOT EXISTS recordings (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        record_id TEXT UNIQUE NOT NULL,
        meeting_id TEXT NOT NULL,
        name TEXT,
        state TEXT,
        published INTEGER DEFAULT 1,
        start_time TEXT,
        end_time TEXT,
        participants TEXT,
        size TEXT,
        formats TEXT NOT NULL DEFAULT '[]',
        metadata TEXT DEFAULT '{}',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
      );
      CREATE INDEX IF NOT EXISTS idx_recordings_record_id ON recordings(record_id);
      CREATE INDEX IF NOT EXISTS idx_recordings_meeting_id ON recordings(meeting_id);
    `);
  }

  // ── Recording transcription / AI summary ────────────────────────────────
  if (!(await db.columnExists('rooms', 'recording_transcript'))) {
    await db.exec('ALTER TABLE rooms ADD COLUMN recording_transcript INTEGER DEFAULT 0');
  }
  if (!(await db.columnExists('recordings', 'transcript_requested_at'))) {
    if (isPostgres) {
      await db.exec('ALTER TABLE recordings ADD COLUMN transcript_requested_at TIMESTAMP DEFAULT NULL');
    } else {
      await db.exec('ALTER TABLE recordings ADD COLUMN transcript_requested_at DATETIME DEFAULT NULL');
    }
  }

  // ── Default admin (only on very first start) ────────────────────────────
  const adminAlreadySeeded = await db.get("SELECT value FROM settings WHERE key = 'admin_seeded'");
  if (!adminAlreadySeeded) {
    const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com';
    const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';

    // Check if admin already exists (upgrade from older version without the flag)
    const existing = await db.get('SELECT id FROM users WHERE email = ?', [adminEmail]);
    if (!existing) {
      const hash = bcrypt.hashSync(adminPassword, 12);
      await db.run(
        'INSERT INTO users (name, display_name, email, password_hash, role, email_verified) VALUES (?, ?, ?, ?, ?, 1)',
        ['Administrator', 'Administrator', adminEmail, hash, 'admin']
      );
      log.db.info(`Default admin created: ${adminEmail}`);
    }
    // Mark as seeded so it never runs again, even if the admin email is changed
    await db.run("INSERT INTO settings (key, value) VALUES ('admin_seeded', '1') RETURNING key");
  }
}
