/**
 * Greenlight → Redlight Migration Script
 *
 * Migrates users, rooms (including settings and shared accesses),
 * site settings (branding, registration), logo, and OAuth/OIDC configuration
 * from a Greenlight v3 PostgreSQL database into a Redlight instance
 * (SQLite or PostgreSQL).
 *
 * Usage:
 *   node migrate-from-greenlight.mjs [options]
 *
 * Options:
 *   --dry-run        Show what would be migrated without writing anything
 *   --skip-rooms     Only migrate users, not rooms
 *   --skip-shares    Do not migrate room share (shared_accesses)
 *   --skip-settings  Do not migrate site_settings / branding
 *   --skip-oauth     Do not migrate OAuth / OIDC configuration
 *   --verbose        Print every imported row
 *
 * Environment variables (can also be in .env):
 *
 *   Source (Greenlight DB — PostgreSQL):
 *     GL_DATABASE_URL   Full Postgres connection string
 *                       e.g. postgres://gl_user:pass@localhost/greenlight_db
 *
 *   Target (Redlight DB — auto-detected):
 *     DATABASE_URL      Set for PostgreSQL target (same format as GL_DATABASE_URL)
 *     SQLITE_PATH       Set (or leave empty) for SQLite target
 *                       Default: ./redlight.db (relative to this script)
 *
 *   OAuth (from Greenlight .env — optional, only if --skip-oauth is NOT set):
 *     GL_OIDC_ISSUER          OIDC issuer URL (e.g. https://keycloak.example.com/realms/myrealm)
 *     GL_OIDC_CLIENT_ID       OIDC client ID
 *     GL_OIDC_CLIENT_SECRET   OIDC client secret
 *     GL_OIDC_DISPLAY_NAME    Button label on the login page (default: "SSO")
 *
 * Password hashes:
 *   Both Greenlight and Redlight use bcrypt, so password_digest is
 *   copied as-is — users can log in with their existing passwords.
 *
 * Site-settings mapping (GL site_settings → Redlight settings):
 *   BrandingImage             → downloads file → uploads/branding/logo.*
 *   PrimaryColor              → (logged, not mapped — Redlight uses themes)
 *   RegistrationMethod        → registration_mode  (open / invite)
 *   Terms                     → imprint_url
 *   PrivacyPolicy             → privacy_url
 *
 * Meeting-option → Redlight field mapping:
 *   record                  → record_meeting
 *   muteOnStart             → mute_on_join
 *   guestPolicy             → require_approval (ASK_MODERATOR = true)
 *   glAnyoneCanStart        → anyone_can_start
 *   glAnyoneJoinAsModerator → all_join_moderator
 *   glViewerAccessCode      → access_code
 *   glModeratorAccessCode   → moderator_code
 */

import 'dotenv/config';
import pg from 'pg';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';

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

// ── helpers ────────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
const DRY_RUN       = args.includes('--dry-run');
const SKIP_ROOMS    = args.includes('--skip-rooms');
const SKIP_SHARES   = args.includes('--skip-shares');
const SKIP_SETTINGS = args.includes('--skip-settings');
const SKIP_OAUTH    = args.includes('--skip-oauth');
const VERBOSE       = args.includes('--verbose');

const c = {
  reset: '\x1b[0m',
  bold:  '\x1b[1m',
  green: '\x1b[32m',
  yellow:'\x1b[33m',
  red:   '\x1b[31m',
  cyan:  '\x1b[36m',
  dim:   '\x1b[2m',
};

const log   = (...a) => console.log(...a);
const ok    = (msg) => log(`  ${c.green}✓${c.reset} ${msg}`);
const warn  = (msg) => log(`  ${c.yellow}⚠${c.reset}  ${msg}`);
const skip  = (msg) => log(`  ${c.dim}–  ${msg}${c.reset}`);
const err   = (msg) => log(`  ${c.red}✗${c.reset} ${msg}`);
const info  = (msg) => log(`  ${c.cyan}i${c.reset}  ${msg}`);
const loud  = (msg) => { if (VERBOSE) log(`    ${c.dim}${msg}${c.reset}`); };

// ── Source DB (Greenlight PostgreSQL) ─────────────────────────────────────
const GL_URL = process.env.GL_DATABASE_URL;
if (!GL_URL) {
  err('GL_DATABASE_URL is not set. Please set it in your .env file or environment.');
  err('  Example: GL_DATABASE_URL=postgres://gl_user:pass@localhost/greenlight_production');
  process.exit(1);
}

// ── Target DB (Redlight) ───────────────────────────────────────────────────
const RL_URL = process.env.DATABASE_URL;
const isPostgresTarget = !!(RL_URL && RL_URL.startsWith('postgres'));

// ── SQLite adapter (only loaded when needed) ───────────────────────────────
async function openSqlite() {
  const require = createRequire(import.meta.url);
  let Database;
  try {
    Database = require('better-sqlite3');
  } catch {
    err('better-sqlite3 is not installed. Run: npm install better-sqlite3');
    process.exit(1);
  }
  const dbPath = process.env.SQLITE_PATH || './redlight.db';
  const db = Database(dbPath);
  db.pragma('journal_mode = WAL');
  db.pragma('foreign_keys = ON');
  return {
    async get(sql, params = [])  { return db.prepare(sql).get(...params); },
    async all(sql, params = [])  { return db.prepare(sql).all(...params); },
    async run(sql, params = [])  {
      const r = db.prepare(sql).run(...params);
      return { lastInsertRowid: Number(r.lastInsertRowid), changes: r.changes };
    },
    close() { db.close(); },
    type: 'sqlite',
    path: dbPath,
  };
}

// ── PostgreSQL adapter ─────────────────────────────────────────────────────
async function openPostgres(url, label) {
  const pool = new pg.Pool({ connectionString: url });
  let index = 0;
  const convert = (sql) => sql.replace(/\?/g, () => `$${++index}`);
  return {
    async get(sql, params = [])  {
      index = 0;
      const r = await pool.query(convert(sql), params);
      return r.rows[0];
    },
    async all(sql, params = [])  {
      index = 0;
      const r = await pool.query(convert(sql), params);
      return r.rows;
    },
    async run(sql, params = [])  {
      index = 0;
      let q = convert(sql);
      if (/^\s*INSERT/i.test(q) && !/RETURNING/i.test(q)) q += ' RETURNING id';
      const r = await pool.query(q, params);
      return { lastInsertRowid: r.rows[0]?.id, changes: r.rowCount };
    },
    async end() { await pool.end(); },
    type: 'postgres',
  };
}

// ── meeting option → redlight field map ───────────────────────────────────
const OPTION_MAP = {
  record:                   'record_meeting',
  muteOnStart:              'mute_on_join',
  guestPolicy:              'require_approval',      // special: "ASK_MODERATOR"
  glAnyoneCanStart:         'anyone_can_start',
  glAnyoneJoinAsModerator:  'all_join_moderator',
  glViewerAccessCode:       'access_code',
  glModeratorAccessCode:    'moderator_code',
};

function boolOption(val) {
  return val === 'true' || val === '1' ? 1 : 0;
}

// ── AES-256-GCM encryption (matching Redlight's oauth.js) ─────────────────
const ENCRYPTION_KEY = crypto
  .createHash('sha256')
  .update(process.env.JWT_SECRET || '')
  .digest();

function encryptSecret(plaintext) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag().toString('hex');
  return `${iv.toString('hex')}:${authTag}:${encrypted}`;
}

// ── Helper: upsert a setting into the Redlight settings table ─────────────
async function upsertSetting(db, key, value, isPostgres) {
  const existing = await db.get('SELECT key FROM settings WHERE key = ?', [key]);
  if (existing) {
    await db.run('UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?', [value, key]);
  } else {
    await db.run('INSERT INTO settings (key, value) VALUES (?, ?) RETURNING key', [key, value]);
  }
}

// ── main ───────────────────────────────────────────────────────────────────
async function main() {
  log();
  log(`${c.bold}Greenlight → Redlight Migration${c.reset}`);
  if (DRY_RUN) log(`${c.yellow}${c.bold}  DRY RUN — nothing will be written${c.reset}`);
  log();

  // Connect to source
  let glDb;
  try {
    glDb = await openPostgres(GL_URL, 'Greenlight');
    await glDb.get('SELECT 1');
    info(`Connected to Greenlight DB (PostgreSQL)`);
  } catch (e) {
    err(`Cannot connect to Greenlight DB: ${e.message}`);
    process.exit(1);
  }

  // Connect to target
  let rlDb;
  try {
    if (isPostgresTarget) {
      rlDb = await openPostgres(RL_URL, 'Redlight');
      await rlDb.get('SELECT 1');
      info(`Connected to Redlight DB (PostgreSQL)`);
    } else {
      rlDb = await openSqlite();
      info(`Connected to Redlight DB (SQLite: ${rlDb.path})`);
    }
  } catch (e) {
    err(`Cannot connect to Redlight DB: ${e.message}`);
    process.exit(1);
  }
  log();

  // ── 1. Load Greenlight roles ───────────────────────────────────────────
  const roles = await glDb.all('SELECT id, name FROM roles');
  const adminRoleIds = new Set(
    roles.filter(r => /admin|administrator/i.test(r.name)).map(r => r.id)
  );
  info(`Found ${roles.length} roles (${adminRoleIds.size} admin role(s))`);

  // ── 2. Load Greenlight users (local + OIDC) ────────────────────────────
  const glUsers = await glDb.all(`
    SELECT id, name, email, password_digest, language, role_id, verified, status, provider, external_id
    FROM users
    ORDER BY created_at ASC
  `);
  const localUsers = glUsers.filter(u => u.provider === 'greenlight');
  const oidcUsers  = glUsers.filter(u => u.provider !== 'greenlight' && u.provider !== null);
  info(`Found ${localUsers.length} local user(s), ${oidcUsers.length} OIDC/SSO user(s)`);
  log();

  // ── 3. Migrate local users ─────────────────────────────────────────────
  log(`${c.bold}Local Users${c.reset}`);
  const userIdMap = new Map(); // gl user id → rl user id
  let usersCreated = 0, usersSkipped = 0;

  for (const u of localUsers) {
    if (!u.password_digest) {
      warn(`${u.email} — no password, skipping`);
      usersSkipped++;
      continue;
    }

    const role = adminRoleIds.has(u.role_id) ? 'admin' : 'user';
    const emailVerified = u.verified ? 1 : 0;
    const lang = u.language || 'de';
    const displayName = u.name || '';

    const existing = await rlDb.get('SELECT id FROM users WHERE email = ?', [u.email]);

    if (existing) {
      userIdMap.set(u.id, existing.id);
      skip(`${u.email} — already exists (id=${existing.id}), skipping`);
      usersSkipped++;
      continue;
    }

    loud(`INSERT user ${u.email} (role=${role})`);
    if (!DRY_RUN) {
      const result = await rlDb.run(
        `INSERT INTO users (name, display_name, email, password_hash, role, language, email_verified)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
        [displayName, displayName, u.email, u.password_digest, role, lang, emailVerified]
      );
      userIdMap.set(u.id, result.lastInsertRowid);
      ok(`${u.email} (${role})`);
    } else {
      ok(`[dry] ${u.email} (${role})`);
    }
    usersCreated++;
  }

  log();
  log(`  Created: ${c.green}${usersCreated}${c.reset}  Skipped: ${usersSkipped}`);
  log();

  // ── 3b. Migrate OIDC / SSO users ───────────────────────────────────────
  log(`${c.bold}OIDC / SSO Users${c.reset}`);
  let oidcCreated = 0, oidcSkipped = 0;

  for (const u of oidcUsers) {
    const role = adminRoleIds.has(u.role_id) ? 'admin' : 'user';
    const lang = u.language || 'de';
    const displayName = u.name || '';
    const email = (u.email || '').toLowerCase().trim();
    if (!email) {
      warn(`OIDC user id=${u.id} — no email, skipping`);
      oidcSkipped++;
      continue;
    }

    const existing = await rlDb.get('SELECT id FROM users WHERE email = ?', [email]);

    if (existing) {
      userIdMap.set(u.id, existing.id);
      // Link OAuth provider if not set yet
      if (!DRY_RUN && u.external_id) {
        await rlDb.run(
          'UPDATE users SET oauth_provider = ?, oauth_provider_id = ?, email_verified = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND oauth_provider IS NULL',
          ['oidc', u.external_id, existing.id]
        );
      }
      skip(`${email} — already exists (id=${existing.id}), linked OIDC`);
      oidcSkipped++;
      continue;
    }

    // Generate a random unusable password hash for OIDC users
    const randomHash = `oauth:${crypto.randomUUID()}`;

    loud(`INSERT OIDC user ${email} (role=${role}, sub=${u.external_id || '?'})`);
    if (!DRY_RUN) {
      const result = await rlDb.run(
        `INSERT INTO users (name, display_name, email, password_hash, role, language, email_verified, oauth_provider, oauth_provider_id)
         VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
        [displayName, displayName, email, randomHash, role, lang, 'oidc', u.external_id || null]
      );
      userIdMap.set(u.id, result.lastInsertRowid);
      ok(`${email} (${role}, OIDC)`);
    } else {
      ok(`[dry] ${email} (${role}, OIDC)`);
    }
    oidcCreated++;
  }

  log();
  log(`  Created: ${c.green}${oidcCreated}${c.reset}  Skipped: ${oidcSkipped}`);
  log();

  if (SKIP_ROOMS) {
    log(`${c.yellow}--skip-rooms set, stopping here.${c.reset}`);
    await cleanup(glDb, rlDb);
    return;
  }

  // ── 4. Load Greenlight rooms + meeting options ─────────────────────────
  log(`${c.bold}Rooms${c.reset}`);

  const glRooms = await glDb.all(`
    SELECT r.id, r.name, r.meeting_id, r.user_id
    FROM rooms r
    ORDER BY r.created_at ASC
  `);

  // Load all meeting options (key name list)
  const meetingOptions = await glDb.all('SELECT id, name FROM meeting_options');
  const optionIdToName = new Map(meetingOptions.map(m => [m.id, m.name]));

  // Load all room_meeting_option values in one shot
  const roomOptionRows = await glDb.all('SELECT room_id, meeting_option_id, value FROM room_meeting_options');
  const roomOptions = new Map(); // room_id → { optionName: value }
  for (const row of roomOptionRows) {
    const name = optionIdToName.get(row.meeting_option_id);
    if (!name) continue;
    if (!roomOptions.has(row.room_id)) roomOptions.set(row.room_id, {});
    roomOptions.get(row.room_id)[name] = row.value;
  }

  info(`Found ${glRooms.length} room(s) across all users`);
  log();

  let roomsCreated = 0, roomsSkipped = 0;
  const roomIdMap = new Map(); // gl room id → rl room id

  for (const room of glRooms) {
    // Determine the redlight owner
    const rlUserId = userIdMap.get(room.user_id);
    if (!rlUserId  && !DRY_RUN) {
      // Try to look up the email in redlight directly in case user already existed
      const glUser = glUsers.find(u => u.id === room.user_id);
      if (glUser) {
        const ex = await rlDb.get('SELECT id FROM users WHERE email = ?', [glUser.email]);
        if (ex) {
          userIdMap.set(room.user_id, ex.id);
        } else {
          warn(`Room "${room.name}" — owner not found in Redlight, skipping`);
          roomsSkipped++;
          continue;
        }
      } else {
        warn(`Room "${room.name}" — owner not found in Greenlight users, skipping`);
        roomsSkipped++;
        continue;
      }
    }

    const ownerId = userIdMap.get(room.user_id) || null;

    // Use Greenlight meeting_id as the uid (preserves BBB meeting identity)
    // Greenlight meeting_ids can be longer, but Redlight stores uid as TEXT — no problem.
    const uid = room.meeting_id;

    // Check if already in Redlight
    const existingRoom = await rlDb.get('SELECT id FROM rooms WHERE uid = ?', [uid]);
    if (existingRoom) {
      roomIdMap.set(room.id, existingRoom.id);
      skip(`"${room.name}" (${uid.substring(0, 12)}…) — already exists`);
      roomsSkipped++;
      continue;
    }

    // Map meeting options to Redlight fields
    const opts = roomOptions.get(room.id) || {};

    const mute_on_join         = opts.muteOnStart            ? boolOption(opts.muteOnStart)           : 1;
    const record_meeting       = opts.record                  ? boolOption(opts.record)                : 1;
    const anyone_can_start     = opts.glAnyoneCanStart        ? boolOption(opts.glAnyoneCanStart)      : 0;
    const all_join_moderator   = opts.glAnyoneJoinAsModerator ? boolOption(opts.glAnyoneJoinAsModerator): 0;
    const require_approval     = opts.guestPolicy === 'ASK_MODERATOR' ? 1 : 0;
    const access_code          = (opts.glViewerAccessCode    && opts.glViewerAccessCode !== 'false')
                                   ? opts.glViewerAccessCode    : null;
    const moderator_code       = (opts.glModeratorAccessCode && opts.glModeratorAccessCode !== 'false')
                                   ? opts.glModeratorAccessCode : null;
    const guest_access         = 1; // Default open like Greenlight

    // Ensure room name meets Redlight 2-char minimum
    const roomName = (room.name || 'Room').length >= 2 ? room.name : (room.name || 'Room').padEnd(2, ' ');

    loud(`INSERT room "${roomName}" uid=${uid.substring(0, 16)}… owner=${ownerId}`);

    if (!DRY_RUN && ownerId) {
      const result = await rlDb.run(
        `INSERT INTO rooms (uid, name, user_id, mute_on_join, record_meeting, anyone_can_start,
          all_join_moderator, require_approval, access_code, moderator_code, guest_access)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        [uid, roomName, ownerId, mute_on_join, record_meeting, anyone_can_start,
         all_join_moderator, require_approval, access_code, moderator_code, guest_access]
      );
      roomIdMap.set(room.id, result.lastInsertRowid);
      const optSummary = [
        mute_on_join       ? 'muted'    : null,
        record_meeting     ? 'record'   : null,
        anyone_can_start   ? 'anyStart' : null,
        all_join_moderator ? 'allMod'   : null,
        require_approval   ? 'approval' : null,
        access_code        ? 'code:'+access_code.substring(0,4)+'…' : null,
      ].filter(Boolean).join(', ');
      ok(`"${roomName}"${optSummary ? ` [${optSummary}]` : ''}`);
    } else if (DRY_RUN) {
      ok(`[dry] "${roomName}" (uid=${uid.substring(0, 16)}…)`);
    } else {
      warn(`"${roomName}" — skipped (no owner resolved)`);
      roomsSkipped++;
      continue;
    }
    roomsCreated++;
  }

  log();
  log(`  Created: ${c.green}${roomsCreated}${c.reset}  Skipped: ${roomsSkipped}`);
  log();

  // ── 5. Migrate shared_accesses ─────────────────────────────────────────
  if (!SKIP_SHARES) {
    log(`${c.bold}Room Shares${c.reset}`);
    const shares = await glDb.all('SELECT user_id, room_id FROM shared_accesses');
    info(`Found ${shares.length} shared accesse(s)`);
    log();

    let sharesCreated = 0, sharesSkipped = 0;
    for (const s of shares) {
      const rlUser = userIdMap.get(s.user_id);
      const rlRoom = roomIdMap.get(s.room_id);

      if (!rlUser || !rlRoom) {
        loud(`Skip share user=${s.user_id} room=${s.room_id} — not found in target`);
        sharesSkipped++;
        continue;
      }

      const exists = await rlDb.get(
        'SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?',
        [rlRoom, rlUser]
      );
      if (exists) {
        sharesSkipped++;
        continue;
      }

      if (!DRY_RUN) {
        await rlDb.run(
          'INSERT INTO room_shares (room_id, user_id) VALUES (?, ?)',
          [rlRoom, rlUser]
        );
        ok(`Share room_id=${rlRoom} → user_id=${rlUser}`);
      } else {
        ok(`[dry] Share room_id=${rlRoom} → user_id=${rlUser}`);
      }
      sharesCreated++;
    }
    log();
    log(`  Created: ${c.green}${sharesCreated}${c.reset}  Skipped: ${sharesSkipped}`);
    log();
  }

  // ── 6. Migrate site_settings / branding ────────────────────────────────
  if (!SKIP_SETTINGS) {
    log(`${c.bold}Site Settings & Branding${c.reset}`);
    let settingsCount = 0;

    // Check if site_settings table exists in Greenlight
    let hasSiteSettings = false;
    try {
      await glDb.get('SELECT 1 FROM site_settings LIMIT 1');
      hasSiteSettings = true;
    } catch {
      warn('No site_settings table found in Greenlight DB — skipping settings migration');
    }

    if (hasSiteSettings) {
      const glSettings = await glDb.all('SELECT setting, value FROM site_settings');
      const settingsMap = new Map(glSettings.map(s => [s.setting, s.value]));
      info(`Found ${glSettings.length} site_setting(s) in Greenlight`);

      // ── Registration mode ──────────────────────────────────────────────
      const regMethod = settingsMap.get('RegistrationMethod');
      if (regMethod) {
        // Greenlight: "open", "invite", "approval"  →  Redlight: "open" or "invite"
        const mode = regMethod === 'open' ? 'open' : 'invite';
        if (!DRY_RUN) await upsertSetting(rlDb, 'registration_mode', mode, isPostgresTarget);
        ok(`registration_mode → ${mode} (was: ${regMethod})`);
        settingsCount++;
      }

      // ── Privacy policy URL ─────────────────────────────────────────────
      const privacy = settingsMap.get('PrivacyPolicy');
      if (privacy && privacy.trim()) {
        if (!DRY_RUN) await upsertSetting(rlDb, 'privacy_url', privacy.trim(), isPostgresTarget);
        ok(`privacy_url → ${privacy.trim()}`);
        settingsCount++;
      }

      // ── Terms / Imprint URL ────────────────────────────────────────────
      const terms = settingsMap.get('Terms');
      if (terms && terms.trim()) {
        if (!DRY_RUN) await upsertSetting(rlDb, 'imprint_url', terms.trim(), isPostgresTarget);
        ok(`imprint_url → ${terms.trim()}`);
        settingsCount++;
      }

      // ── Primary color (informational only — Redlight uses themes) ─────
      const primaryColor = settingsMap.get('PrimaryColor');
      if (primaryColor) {
        info(`PrimaryColor in Greenlight was: ${primaryColor} (not mapped — Redlight uses themes)`);
      }

      // ── Branding image / logo ──────────────────────────────────────────
      const brandingImage = settingsMap.get('BrandingImage');
      if (brandingImage && brandingImage.trim()) {
        const logoUrl = brandingImage.trim();
        info(`BrandingImage URL: ${logoUrl}`);

        if (!DRY_RUN) {
          try {
            const response = await fetch(logoUrl, { signal: AbortSignal.timeout(15_000) });
            if (response.ok) {
              const contentType = response.headers.get('content-type') || '';
              let ext = '.png';
              if (contentType.includes('svg')) ext = '.svg';
              else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = '.jpg';
              else if (contentType.includes('gif')) ext = '.gif';
              else if (contentType.includes('webp')) ext = '.webp';
              else if (contentType.includes('ico')) ext = '.ico';

              const brandingDir = path.join(__dirname, 'uploads', 'branding');
              fs.mkdirSync(brandingDir, { recursive: true });

              // Remove old logos
              if (fs.existsSync(brandingDir)) {
                for (const f of fs.readdirSync(brandingDir)) {
                  if (f.startsWith('logo.')) fs.unlinkSync(path.join(brandingDir, f));
                }
              }

              const logoPath = path.join(brandingDir, `logo${ext}`);
              const buffer = Buffer.from(await response.arrayBuffer());
              fs.writeFileSync(logoPath, buffer);
              ok(`Logo saved → uploads/branding/logo${ext} (${(buffer.length / 1024).toFixed(1)} KB)`);
              settingsCount++;
            } else {
              warn(`Could not download logo (HTTP ${response.status}) — skipping`);
            }
          } catch (dlErr) {
            warn(`Logo download failed: ${dlErr.message}`);
          }
        } else {
          ok(`[dry] Would download logo from ${logoUrl}`);
          settingsCount++;
        }
      }

      log();
      log(`  Settings migrated: ${c.green}${settingsCount}${c.reset}`);
      log();
    }
  }

  // ── 7. Migrate OAuth / OIDC configuration ──────────────────────────────
  if (!SKIP_OAUTH) {
    log(`${c.bold}OAuth / OIDC Configuration${c.reset}`);

    const issuer      = process.env.GL_OIDC_ISSUER;
    const clientId    = process.env.GL_OIDC_CLIENT_ID;
    const clientSecret = process.env.GL_OIDC_CLIENT_SECRET;
    const displayName = process.env.GL_OIDC_DISPLAY_NAME || 'SSO';

    if (issuer && clientId && clientSecret) {
      if (!process.env.JWT_SECRET) {
        warn('JWT_SECRET is not set — cannot encrypt client secret. Skipping OAuth migration.');
      } else {
        // Check if already configured
        const existing = await rlDb.get("SELECT key FROM settings WHERE key = 'oauth_config'");
        if (existing) {
          warn('oauth_config already exists in Redlight — skipping (delete it first to re-migrate)');
        } else {
          info(`Issuer:       ${issuer}`);
          info(`Client ID:    ${clientId}`);
          info(`Display name: ${displayName}`);

          if (!DRY_RUN) {
            const config = JSON.stringify({
              issuer,
              clientId,
              encryptedSecret: encryptSecret(clientSecret),
              displayName,
              autoRegister: true,
            });
            await upsertSetting(rlDb, 'oauth_config', config, isPostgresTarget);
            ok('OAuth/OIDC configuration saved');
          } else {
            ok('[dry] Would save OAuth/OIDC configuration');
          }
        }
      }
    } else if (issuer || clientId || clientSecret) {
      warn('Incomplete OIDC config — set GL_OIDC_ISSUER, GL_OIDC_CLIENT_ID, and GL_OIDC_CLIENT_SECRET');
    } else {
      info('No GL_OIDC_* env vars set — skipping OAuth migration');
      info('Set GL_OIDC_ISSUER, GL_OIDC_CLIENT_ID, GL_OIDC_CLIENT_SECRET to migrate OIDC config');
    }
    log();
  }

  // ── Summary ────────────────────────────────────────────────────────────
  log(`${c.bold}${c.green}Migration complete${c.reset}`);
  log();
  log(`  Local users migrated: ${c.green}${usersCreated}${c.reset}`);
  log(`  OIDC users migrated:  ${c.green}${oidcCreated}${c.reset}`);
  log(`  Rooms migrated:       ${c.green}${roomsCreated}${c.reset}`);
  if (!SKIP_SHARES)   log(`  Shares migrated:      (see above)`);
  if (!SKIP_SETTINGS) log(`  Settings migrated:    (see above)`);
  if (!SKIP_OAUTH)    log(`  OAuth config:         (see above)`);
  if (DRY_RUN) {
    log();
    log(`  ${c.yellow}${c.bold}This was a DRY RUN — rerun without --dry-run to apply.${c.reset}`);
  }
  log();

  await cleanup(glDb, rlDb);
}

async function cleanup(glDb, rlDb) {
  try { await glDb.end?.(); } catch {}
  try { await rlDb.end?.(); rlDb.close?.(); } catch {}
}

main().catch(e => {
  console.error(`\n${c.red}Fatal error:${c.reset}`, e.message);
  process.exit(1);
});
