import 'dotenv/config';
import { db, dbAll, events } from './index.js';
import { sql, eq } from 'drizzle-orm';
import { uniqueSlug } from '../lib/slugify.js';

const dbType = process.env.DB_TYPE || 'sqlite';
console.log(`Database type: ${dbType}`);
// Do not log DATABASE_URL: it may contain credentials.

async function migrate() {
  console.log('Running migrations...');
  
  if (dbType === 'sqlite') {
    // SQLite migrations
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS users (
        id TEXT PRIMARY KEY,
        email TEXT NOT NULL UNIQUE,
        password TEXT,
        name TEXT NOT NULL,
        phone TEXT,
        role TEXT NOT NULL DEFAULT 'user',
        language_preference TEXT,
        is_claimed INTEGER NOT NULL DEFAULT 1,
        google_id TEXT,
        ruc_number TEXT,
        account_status TEXT NOT NULL DEFAULT 'active',
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    // Add new user columns if they don't exist (for existing databases)
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN is_claimed INTEGER NOT NULL DEFAULT 1`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN google_id TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN ruc_number TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN account_status TEXT NOT NULL DEFAULT 'active'`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }

    // Magic link tokens table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS magic_link_tokens (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL REFERENCES users(id),
        token TEXT NOT NULL UNIQUE,
        type TEXT NOT NULL,
        expires_at TEXT NOT NULL,
        used_at TEXT,
        created_at TEXT NOT NULL
      )
    `);

    // User sessions table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS user_sessions (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL REFERENCES users(id),
        token TEXT NOT NULL UNIQUE,
        user_agent TEXT,
        ip_address TEXT,
        last_active_at TEXT NOT NULL,
        expires_at TEXT NOT NULL,
        created_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        title_es TEXT,
        description TEXT NOT NULL,
        description_es TEXT,
        short_description TEXT,
        short_description_es TEXT,
        start_datetime TEXT NOT NULL,
        end_datetime TEXT,
        location TEXT NOT NULL,
        location_url TEXT,
        price REAL NOT NULL DEFAULT 0,
        currency TEXT NOT NULL DEFAULT 'PYG',
        capacity INTEGER NOT NULL DEFAULT 50,
        status TEXT NOT NULL DEFAULT 'draft',
        banner_url TEXT,
        external_booking_enabled INTEGER NOT NULL DEFAULT 0,
        external_booking_url TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    // Add external booking columns to events if they don't exist (for existing databases)
    try {
      await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_enabled INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_url TEXT`);
    } catch (e) { /* column may already exist */ }

    // Add short description columns to events
    try {
      await (db as any).run(sql`ALTER TABLE events ADD COLUMN short_description TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE events ADD COLUMN short_description_es TEXT`);
    } catch (e) { /* column may already exist */ }

    // Add slug column to events (backfilled below)
    try {
      await (db as any).run(sql`ALTER TABLE events ADD COLUMN slug TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`CREATE UNIQUE INDEX IF NOT EXISTS events_slug_unique ON events(slug)`);
    } catch (e) { /* index may already exist */ }

    // Historical slugs that still resolve (and redirect) to an event's canonical slug
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS event_slug_aliases (
        slug TEXT PRIMARY KEY,
        event_id TEXT NOT NULL REFERENCES events(id),
        created_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      -- Matches db/schema.ts. The legacy attendee_name / NOT NULL email+phone
      -- shape only survives in databases created before the split into
      -- first/last name, where the ALTERs below relaxed it; a fresh database
      -- must not recreate constraints the app no longer satisfies (door
      -- walk-ins have neither an email nor a phone).
      CREATE TABLE IF NOT EXISTS tickets (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL REFERENCES users(id),
        event_id TEXT NOT NULL REFERENCES events(id),
        attendee_first_name TEXT NOT NULL,
        attendee_last_name TEXT,
        attendee_email TEXT,
        attendee_phone TEXT,
        attendee_ruc TEXT,
        preferred_language TEXT,
        status TEXT NOT NULL DEFAULT 'pending',
        checkin_at TEXT,
        qr_code TEXT,
        admin_note TEXT,
        created_at TEXT NOT NULL
      )
    `);

    // Add new columns if they don't exist (for existing databases)
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_name TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_email TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_phone TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN preferred_language TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN admin_note TEXT`);
    } catch (e) { /* column may already exist */ }
    
    // Migration: Add first/last name columns
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_first_name TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_last_name TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc TEXT`);
    } catch (e) { /* column may already exist */ }
    
    // Migration: Copy data from attendee_name to attendee_first_name if attendee_first_name is empty
    try {
      await (db as any).run(sql`
        UPDATE tickets 
        SET attendee_first_name = attendee_name 
        WHERE attendee_first_name IS NULL AND attendee_name IS NOT NULL
      `);
    } catch (e) { /* migration may have already run */ }
    
    // Migration: Add checked_in_by_admin_id column to tickets
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN checked_in_by_admin_id TEXT REFERENCES users(id)`);
    } catch (e) { /* column may already exist */ }
    
    // Migration: Add booking_id column to tickets for multi-ticket bookings
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN booking_id TEXT`);
    } catch (e) { /* column may already exist */ }

    // Migration: Add is_guest column to tickets
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN is_guest INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }

    // Migration: Add payment_status column to tickets (paid | unpaid | comp),
    // backfilled from is_guest and the payments table on first run
    try {
      await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN payment_status TEXT NOT NULL DEFAULT 'unpaid'`);
      await (db as any).run(sql`UPDATE tickets SET payment_status = 'comp' WHERE is_guest = 1`);
      await (db as any).run(sql`
        UPDATE tickets SET payment_status = 'paid'
        WHERE is_guest = 0
          AND id IN (SELECT ticket_id FROM payments WHERE status = 'paid')
      `);
    } catch (e) { /* column may already exist */ }

    // Make attendee_email and attendee_phone nullable (recreate table if needed or just allow nulls for new entries)
    // SQLite doesn't support altering column constraints, so we'll just ensure new entries work

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS payments (
        id TEXT PRIMARY KEY,
        ticket_id TEXT NOT NULL REFERENCES tickets(id),
        provider TEXT NOT NULL,
        amount REAL NOT NULL,
        currency TEXT NOT NULL DEFAULT 'PYG',
        status TEXT NOT NULL DEFAULT 'pending',
        reference TEXT,
        paid_at TEXT,
        paid_by_admin_id TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    // Add new columns if they don't exist (for existing databases)
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN paid_at TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN paid_by_admin_id TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN user_marked_paid_at TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN admin_note TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN payer_name TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN reminder_sent_at TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_invoice TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_expires_at TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_amount_sats INTEGER`);
    } catch (e) { /* column may already exist */ }
    // Door check-in screen: split pre-sale vs door revenue and record the tender
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN source TEXT NOT NULL DEFAULT 'presale'`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE payments ADD COLUMN method TEXT`);
    } catch (e) { /* column may already exist */ }

    // Idempotency records for door check-in actions (retries / double taps)
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS idempotency_keys (
        key TEXT PRIMARY KEY,
        scope TEXT NOT NULL,
        result TEXT NOT NULL,
        undo_state TEXT,
        undone_at TEXT,
        created_at TEXT NOT NULL
      )
    `);

    // Invoices table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS invoices (
        id TEXT PRIMARY KEY,
        payment_id TEXT NOT NULL REFERENCES payments(id),
        user_id TEXT NOT NULL REFERENCES users(id),
        invoice_number TEXT NOT NULL UNIQUE,
        ruc_number TEXT,
        legal_name TEXT,
        amount REAL NOT NULL,
        currency TEXT NOT NULL DEFAULT 'PYG',
        pdf_url TEXT,
        status TEXT NOT NULL DEFAULT 'generated',
        created_at TEXT NOT NULL
      )
    `);

    // Payment options table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS payment_options (
        id TEXT PRIMARY KEY,
        tpago_enabled INTEGER NOT NULL DEFAULT 0,
        tpago_link TEXT,
        tpago_link_2 TEXT,
        tpago_link_3 TEXT,
        tpago_link_4 TEXT,
        tpago_link_5 TEXT,
        tpago_instructions TEXT,
        tpago_instructions_es TEXT,
        bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
        bank_name TEXT,
        bank_account_holder TEXT,
        bank_account_number TEXT,
        bank_alias TEXT,
        bank_phone TEXT,
        bank_notes TEXT,
        bank_notes_es TEXT,
        lightning_enabled INTEGER NOT NULL DEFAULT 1,
        cash_enabled INTEGER NOT NULL DEFAULT 1,
        cash_instructions TEXT,
        cash_instructions_es TEXT,
        updated_at TEXT NOT NULL,
        updated_by TEXT REFERENCES users(id)
      )
    `);

    // Add allow_duplicate_bookings column to payment_options if it doesn't exist
    try {
      await (db as any).run(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }

    // Add per-quantity TPago link columns to payment_options if they don't exist
    for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
      try {
        await (db as any).run(sql.raw(`ALTER TABLE payment_options ADD COLUMN ${col} TEXT`));
      } catch (e) { /* column may already exist */ }
    }

    // Event payment overrides table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS event_payment_overrides (
        id TEXT PRIMARY KEY,
        event_id TEXT NOT NULL REFERENCES events(id),
        tpago_enabled INTEGER,
        tpago_link TEXT,
        tpago_link_2 TEXT,
        tpago_link_3 TEXT,
        tpago_link_4 TEXT,
        tpago_link_5 TEXT,
        tpago_instructions TEXT,
        tpago_instructions_es TEXT,
        bank_transfer_enabled INTEGER,
        bank_name TEXT,
        bank_account_holder TEXT,
        bank_account_number TEXT,
        bank_alias TEXT,
        bank_phone TEXT,
        bank_notes TEXT,
        bank_notes_es TEXT,
        lightning_enabled INTEGER,
        cash_enabled INTEGER,
        cash_instructions TEXT,
        cash_instructions_es TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    // Add per-quantity TPago link columns to event_payment_overrides if they don't exist
    for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
      try {
        await (db as any).run(sql.raw(`ALTER TABLE event_payment_overrides ADD COLUMN ${col} TEXT`));
      } catch (e) { /* column may already exist */ }
    }

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS contacts (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT NOT NULL,
        message TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'new',
        created_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS email_subscribers (
        id TEXT PRIMARY KEY,
        email TEXT NOT NULL UNIQUE,
        name TEXT,
        status TEXT NOT NULL DEFAULT 'active',
        created_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS media (
        id TEXT PRIMARY KEY,
        file_url TEXT NOT NULL,
        type TEXT NOT NULL,
        related_id TEXT,
        related_type TEXT,
        created_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS audit_logs (
        id TEXT PRIMARY KEY,
        user_id TEXT REFERENCES users(id),
        action TEXT NOT NULL,
        target TEXT,
        target_id TEXT,
        details TEXT,
        timestamp TEXT NOT NULL
      )
    `);

    // Email system tables
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS email_templates (
        id TEXT PRIMARY KEY,
        name TEXT NOT NULL UNIQUE,
        slug TEXT NOT NULL UNIQUE,
        subject TEXT NOT NULL,
        subject_es TEXT,
        body_html TEXT NOT NULL,
        body_html_es TEXT,
        body_text TEXT,
        body_text_es TEXT,
        description TEXT,
        variables TEXT,
        is_system INTEGER NOT NULL DEFAULT 0,
        is_active INTEGER NOT NULL DEFAULT 1,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS email_logs (
        id TEXT PRIMARY KEY,
        template_id TEXT REFERENCES email_templates(id),
        event_id TEXT REFERENCES events(id),
        recipient_email TEXT NOT NULL,
        recipient_name TEXT,
        subject TEXT NOT NULL,
        body_html TEXT,
        status TEXT NOT NULL DEFAULT 'pending',
        error_message TEXT,
        sent_at TEXT,
        sent_by TEXT REFERENCES users(id),
        created_at TEXT NOT NULL
      )
    `);

    try {
      await (db as any).run(sql`ALTER TABLE email_logs ADD COLUMN resend_attempts INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE email_logs ADD COLUMN last_resent_at TEXT`);
    } catch (e) { /* column may already exist */ }

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS email_settings (
        id TEXT PRIMARY KEY,
        key TEXT NOT NULL UNIQUE,
        value TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS email_queue (
        id TEXT PRIMARY KEY,
        params TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending',
        attempts INTEGER NOT NULL DEFAULT 0,
        last_error TEXT,
        created_at TEXT NOT NULL,
        processed_at TEXT
      )
    `);

    // Site settings table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS site_settings (
        id TEXT PRIMARY KEY,
        timezone TEXT NOT NULL DEFAULT 'America/Asuncion',
        site_name TEXT NOT NULL DEFAULT 'Spanglish',
        site_description TEXT,
        site_description_es TEXT,
        contact_email TEXT,
        contact_phone TEXT,
        facebook_url TEXT,
        instagram_url TEXT,
        twitter_url TEXT,
        linkedin_url TEXT,
        featured_event_id TEXT REFERENCES events(id),
        maintenance_mode INTEGER NOT NULL DEFAULT 0,
        maintenance_message TEXT,
        maintenance_message_es TEXT,
        updated_at TEXT NOT NULL,
        updated_by TEXT REFERENCES users(id)
      )
    `);

    // Add featured_event_id column to site_settings if it doesn't exist
    try {
      await (db as any).run(sql`ALTER TABLE site_settings ADD COLUMN featured_event_id TEXT REFERENCES events(id)`);
    } catch (e) { /* column may already exist */ }

    // Legal pages table for admin-editable legal content
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS legal_pages (
        id TEXT PRIMARY KEY,
        slug TEXT NOT NULL UNIQUE,
        title TEXT NOT NULL,
        title_es TEXT,
        content_text TEXT NOT NULL,
        content_text_es TEXT,
        content_markdown TEXT NOT NULL,
        content_markdown_es TEXT,
        updated_at TEXT NOT NULL,
        updated_by TEXT REFERENCES users(id),
        created_at TEXT NOT NULL
      )
    `);

    // FAQ questions table
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS faq_questions (
        id TEXT PRIMARY KEY,
        question TEXT NOT NULL,
        question_es TEXT,
        answer TEXT NOT NULL,
        answer_es TEXT,
        enabled INTEGER NOT NULL DEFAULT 1,
        show_on_homepage INTEGER NOT NULL DEFAULT 0,
        rank INTEGER NOT NULL DEFAULT 0,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      )
    `);

    // Legal settings table for legal page placeholder values
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS legal_settings (
        id TEXT PRIMARY KEY,
        company_name TEXT,
        legal_entity_name TEXT,
        ruc_number TEXT,
        company_address TEXT,
        company_city TEXT,
        company_country TEXT,
        support_email TEXT,
        legal_email TEXT,
        governing_law TEXT,
        jurisdiction_city TEXT,
        updated_at TEXT NOT NULL,
        updated_by TEXT REFERENCES users(id)
      )
    `);

    // ==================== Better Auth ====================
    // Better Auth core + admin plugin columns on the existing users table
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN image TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN banned INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN ban_reason TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).run(sql`ALTER TABLE users ADD COLUMN ban_expires INTEGER`);
    } catch (e) { /* column may already exist */ }

    // Better Auth sessions (replaces the legacy user_sessions table).
    // Timestamps are integer epoch-milliseconds (Drizzle timestamp_ms mode).
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS auth_sessions (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        token TEXT NOT NULL UNIQUE,
        expires_at INTEGER NOT NULL,
        ip_address TEXT,
        user_agent TEXT,
        impersonated_by TEXT,
        created_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      )
    `);

    // Better Auth accounts: credential (password hash) and OAuth provider links
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS auth_accounts (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        account_id TEXT NOT NULL,
        provider_id TEXT NOT NULL,
        access_token TEXT,
        refresh_token TEXT,
        id_token TEXT,
        access_token_expires_at INTEGER,
        refresh_token_expires_at INTEGER,
        scope TEXT,
        password TEXT,
        created_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      )
    `);

    // Better Auth verification values (magic links, password reset tokens)
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS auth_verifications (
        id TEXT PRIMARY KEY,
        identifier TEXT NOT NULL,
        value TEXT NOT NULL,
        expires_at INTEGER NOT NULL,
        created_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      )
    `);

    // Better Auth rate limiting (used when Redis is not configured)
    await (db as any).run(sql`
      CREATE TABLE IF NOT EXISTS auth_rate_limits (
        id TEXT PRIMARY KEY,
        key TEXT,
        count INTEGER,
        last_request INTEGER
      )
    `);
  } else {
    // PostgreSQL migrations
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS users (
        id UUID PRIMARY KEY,
        email VARCHAR(255) NOT NULL UNIQUE,
        password VARCHAR(255),
        name VARCHAR(255) NOT NULL,
        phone VARCHAR(50),
        role VARCHAR(20) NOT NULL DEFAULT 'user',
        language_preference VARCHAR(10),
        is_claimed INTEGER NOT NULL DEFAULT 1,
        google_id VARCHAR(255),
        ruc_number VARCHAR(15),
        account_status VARCHAR(20) NOT NULL DEFAULT 'active',
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Add new user columns for existing PostgreSQL databases
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN is_claimed INTEGER NOT NULL DEFAULT 1`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN google_id VARCHAR(255)`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN ruc_number VARCHAR(15)`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN account_status VARCHAR(20) NOT NULL DEFAULT 'active'`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }

    // Magic link tokens table
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS magic_link_tokens (
        id UUID PRIMARY KEY,
        user_id UUID NOT NULL REFERENCES users(id),
        token VARCHAR(255) NOT NULL UNIQUE,
        type VARCHAR(30) NOT NULL,
        expires_at TIMESTAMP NOT NULL,
        used_at TIMESTAMP,
        created_at TIMESTAMP NOT NULL
      )
    `);

    // User sessions table
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS user_sessions (
        id UUID PRIMARY KEY,
        user_id UUID NOT NULL REFERENCES users(id),
        token VARCHAR(500) NOT NULL UNIQUE,
        user_agent TEXT,
        ip_address VARCHAR(45),
        last_active_at TIMESTAMP NOT NULL,
        expires_at TIMESTAMP NOT NULL,
        created_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS events (
        id UUID PRIMARY KEY,
        title VARCHAR(255) NOT NULL,
        title_es VARCHAR(255),
        description TEXT NOT NULL,
        description_es TEXT,
        short_description VARCHAR(300),
        short_description_es VARCHAR(300),
        start_datetime TIMESTAMPTZ NOT NULL,
        end_datetime TIMESTAMPTZ,
        location VARCHAR(500) NOT NULL,
        location_url VARCHAR(500),
        price DECIMAL(10, 2) NOT NULL DEFAULT 0,
        currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
        capacity INTEGER NOT NULL DEFAULT 50,
        status VARCHAR(20) NOT NULL DEFAULT 'draft',
        banner_url VARCHAR(500),
        external_booking_enabled INTEGER NOT NULL DEFAULT 0,
        external_booking_url VARCHAR(500),
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Add external booking columns to events if they don't exist (for existing databases)
    try {
      await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_enabled INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_url VARCHAR(500)`);
    } catch (e) { /* column may already exist */ }

    // Add short description columns to events
    try {
      await (db as any).execute(sql`ALTER TABLE events ADD COLUMN short_description VARCHAR(300)`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE events ADD COLUMN short_description_es VARCHAR(300)`);
    } catch (e) { /* column may already exist */ }

    // Migrate event datetime columns from TIMESTAMP to TIMESTAMPTZ for
    // unambiguous UTC storage (eliminates pg driver timezone interpretation).
    try {
      await (db as any).execute(sql`ALTER TABLE events ALTER COLUMN start_datetime TYPE TIMESTAMPTZ USING start_datetime AT TIME ZONE 'UTC'`);
    } catch (e) { /* already timestamptz or other issue */ }
    try {
      await (db as any).execute(sql`ALTER TABLE events ALTER COLUMN end_datetime TYPE TIMESTAMPTZ USING end_datetime AT TIME ZONE 'UTC'`);
    } catch (e) { /* already timestamptz or other issue */ }

    // Add slug column to events (backfilled below)
    try {
      await (db as any).execute(sql`ALTER TABLE events ADD COLUMN slug VARCHAR(255)`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`CREATE UNIQUE INDEX IF NOT EXISTS events_slug_unique ON events(slug)`);
    } catch (e) { /* index may already exist */ }

    // Historical slugs that still resolve (and redirect) to an event's canonical slug
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS event_slug_aliases (
        slug VARCHAR(255) PRIMARY KEY,
        event_id UUID NOT NULL REFERENCES events(id),
        created_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS tickets (
        id UUID PRIMARY KEY,
        user_id UUID NOT NULL REFERENCES users(id),
        event_id UUID NOT NULL REFERENCES events(id),
        attendee_first_name VARCHAR(255) NOT NULL,
        attendee_last_name VARCHAR(255),
        attendee_email VARCHAR(255),
        attendee_phone VARCHAR(50),
        attendee_ruc VARCHAR(15),
        preferred_language VARCHAR(10),
        status VARCHAR(20) NOT NULL DEFAULT 'pending',
        checkin_at TIMESTAMP,
        qr_code VARCHAR(255),
        admin_note TEXT,
        created_at TIMESTAMP NOT NULL
      )
    `);

    // Add attendee_ruc column if it doesn't exist
    try {
      await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc VARCHAR(15)`);
    } catch (e) { /* column may already exist */ }
    
    // Add checked_in_by_admin_id column to tickets
    try {
      await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN checked_in_by_admin_id UUID REFERENCES users(id)`);
    } catch (e) { /* column may already exist */ }
    
    // Migration: Add booking_id column to tickets for multi-ticket bookings
    try {
      await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN booking_id UUID`);
    } catch (e) { /* column may already exist */ }

    // Migration: Add is_guest column to tickets
    try {
      await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN is_guest INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }

    // Migration: Add payment_status column to tickets (paid | unpaid | comp),
    // backfilled from is_guest and the payments table on first run
    try {
      await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN payment_status VARCHAR(10) NOT NULL DEFAULT 'unpaid'`);
      await (db as any).execute(sql`UPDATE tickets SET payment_status = 'comp' WHERE is_guest = 1`);
      await (db as any).execute(sql`
        UPDATE tickets SET payment_status = 'paid'
        WHERE is_guest = 0
          AND id IN (SELECT ticket_id FROM payments WHERE status = 'paid')
      `);
    } catch (e) { /* column may already exist */ }

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS payments (
        id UUID PRIMARY KEY,
        ticket_id UUID NOT NULL REFERENCES tickets(id),
        provider VARCHAR(50) NOT NULL,
        amount DECIMAL(10, 2) NOT NULL,
        currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
        status VARCHAR(20) NOT NULL DEFAULT 'pending',
        reference VARCHAR(255),
        user_marked_paid_at TIMESTAMP,
        payer_name VARCHAR(255),
        paid_at TIMESTAMP,
        paid_by_admin_id UUID,
        admin_note TEXT,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);
    
    // Add payer_name column if it doesn't exist
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN payer_name VARCHAR(255)`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN reminder_sent_at TIMESTAMP`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_invoice TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_expires_at TIMESTAMP`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_amount_sats INTEGER`);
    } catch (e) { /* column may already exist */ }
    // Door check-in screen: split pre-sale vs door revenue and record the tender
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'presale'`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN method VARCHAR(20)`);
    } catch (e) { /* column may already exist */ }

    // Idempotency records for door check-in actions (retries / double taps)
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS idempotency_keys (
        key VARCHAR(128) PRIMARY KEY,
        scope VARCHAR(64) NOT NULL,
        result TEXT NOT NULL,
        undo_state TEXT,
        undone_at TIMESTAMP,
        created_at TIMESTAMP NOT NULL
      )
    `);

    // Invoices table
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS invoices (
        id UUID PRIMARY KEY,
        payment_id UUID NOT NULL REFERENCES payments(id),
        user_id UUID NOT NULL REFERENCES users(id),
        invoice_number VARCHAR(50) NOT NULL UNIQUE,
        ruc_number VARCHAR(15),
        legal_name VARCHAR(255),
        amount DECIMAL(10, 2) NOT NULL,
        currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
        pdf_url VARCHAR(500),
        status VARCHAR(20) NOT NULL DEFAULT 'generated',
        created_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS payment_options (
        id UUID PRIMARY KEY,
        tpago_enabled INTEGER NOT NULL DEFAULT 0,
        tpago_link VARCHAR(500),
        tpago_link_2 VARCHAR(500),
        tpago_link_3 VARCHAR(500),
        tpago_link_4 VARCHAR(500),
        tpago_link_5 VARCHAR(500),
        tpago_instructions TEXT,
        tpago_instructions_es TEXT,
        bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
        bank_name VARCHAR(255),
        bank_account_holder VARCHAR(255),
        bank_account_number VARCHAR(100),
        bank_alias VARCHAR(100),
        bank_phone VARCHAR(50),
        bank_notes TEXT,
        bank_notes_es TEXT,
        lightning_enabled INTEGER NOT NULL DEFAULT 1,
        cash_enabled INTEGER NOT NULL DEFAULT 1,
        cash_instructions TEXT,
        cash_instructions_es TEXT,
        updated_at TIMESTAMP NOT NULL,
        updated_by UUID REFERENCES users(id)
      )
    `);

    // Add allow_duplicate_bookings column to payment_options if it doesn't exist
    try {
      await (db as any).execute(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }

    // Add per-quantity TPago link columns to payment_options if they don't exist
    for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
      try {
        await (db as any).execute(sql.raw(`ALTER TABLE payment_options ADD COLUMN ${col} VARCHAR(500)`));
      } catch (e) { /* column may already exist */ }
    }

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS event_payment_overrides (
        id UUID PRIMARY KEY,
        event_id UUID NOT NULL REFERENCES events(id),
        tpago_enabled INTEGER,
        tpago_link VARCHAR(500),
        tpago_link_2 VARCHAR(500),
        tpago_link_3 VARCHAR(500),
        tpago_link_4 VARCHAR(500),
        tpago_link_5 VARCHAR(500),
        tpago_instructions TEXT,
        tpago_instructions_es TEXT,
        bank_transfer_enabled INTEGER,
        bank_name VARCHAR(255),
        bank_account_holder VARCHAR(255),
        bank_account_number VARCHAR(100),
        bank_alias VARCHAR(100),
        bank_phone VARCHAR(50),
        bank_notes TEXT,
        bank_notes_es TEXT,
        lightning_enabled INTEGER,
        cash_enabled INTEGER,
        cash_instructions TEXT,
        cash_instructions_es TEXT,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Add per-quantity TPago link columns to event_payment_overrides if they don't exist
    for (const col of ['tpago_link_2', 'tpago_link_3', 'tpago_link_4', 'tpago_link_5']) {
      try {
        await (db as any).execute(sql.raw(`ALTER TABLE event_payment_overrides ADD COLUMN ${col} VARCHAR(500)`));
      } catch (e) { /* column may already exist */ }
    }

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS contacts (
        id UUID PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        email VARCHAR(255) NOT NULL,
        message TEXT NOT NULL,
        status VARCHAR(20) NOT NULL DEFAULT 'new',
        created_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS email_subscribers (
        id UUID PRIMARY KEY,
        email VARCHAR(255) NOT NULL UNIQUE,
        name VARCHAR(255),
        status VARCHAR(20) NOT NULL DEFAULT 'active',
        created_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS media (
        id UUID PRIMARY KEY,
        file_url VARCHAR(500) NOT NULL,
        type VARCHAR(20) NOT NULL,
        related_id UUID,
        related_type VARCHAR(50),
        created_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS audit_logs (
        id UUID PRIMARY KEY,
        user_id UUID REFERENCES users(id),
        action VARCHAR(100) NOT NULL,
        target VARCHAR(100),
        target_id UUID,
        details TEXT,
        timestamp TIMESTAMP NOT NULL
      )
    `);

    // Email system tables
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS email_templates (
        id UUID PRIMARY KEY,
        name VARCHAR(255) NOT NULL UNIQUE,
        slug VARCHAR(100) NOT NULL UNIQUE,
        subject VARCHAR(500) NOT NULL,
        subject_es VARCHAR(500),
        body_html TEXT NOT NULL,
        body_html_es TEXT,
        body_text TEXT,
        body_text_es TEXT,
        description TEXT,
        variables TEXT,
        is_system INTEGER NOT NULL DEFAULT 0,
        is_active INTEGER NOT NULL DEFAULT 1,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS email_logs (
        id UUID PRIMARY KEY,
        template_id UUID REFERENCES email_templates(id),
        event_id UUID REFERENCES events(id),
        recipient_email VARCHAR(255) NOT NULL,
        recipient_name VARCHAR(255),
        subject VARCHAR(500) NOT NULL,
        body_html TEXT,
        status VARCHAR(20) NOT NULL DEFAULT 'pending',
        error_message TEXT,
        sent_at TIMESTAMP,
        sent_by UUID REFERENCES users(id),
        created_at TIMESTAMP NOT NULL
      )
    `);

    try {
      await (db as any).execute(sql`ALTER TABLE email_logs ADD COLUMN resend_attempts INTEGER NOT NULL DEFAULT 0`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE email_logs ADD COLUMN last_resent_at TIMESTAMP`);
    } catch (e) { /* column may already exist */ }

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS email_settings (
        id UUID PRIMARY KEY,
        key VARCHAR(100) NOT NULL UNIQUE,
        value TEXT NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS email_queue (
        id UUID PRIMARY KEY,
        params TEXT NOT NULL,
        status VARCHAR(20) NOT NULL DEFAULT 'pending',
        attempts INTEGER NOT NULL DEFAULT 0,
        last_error TEXT,
        created_at TIMESTAMP NOT NULL,
        processed_at TIMESTAMP
      )
    `);

    // Site settings table
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS site_settings (
        id UUID PRIMARY KEY,
        timezone VARCHAR(100) NOT NULL DEFAULT 'America/Asuncion',
        site_name VARCHAR(255) NOT NULL DEFAULT 'Spanglish',
        site_description TEXT,
        site_description_es TEXT,
        contact_email VARCHAR(255),
        contact_phone VARCHAR(50),
        facebook_url VARCHAR(500),
        instagram_url VARCHAR(500),
        twitter_url VARCHAR(500),
        linkedin_url VARCHAR(500),
        featured_event_id UUID REFERENCES events(id),
        maintenance_mode INTEGER NOT NULL DEFAULT 0,
        maintenance_message TEXT,
        maintenance_message_es TEXT,
        updated_at TIMESTAMP NOT NULL,
        updated_by UUID REFERENCES users(id)
      )
    `);

    // Add featured_event_id column to site_settings if it doesn't exist
    try {
      await (db as any).execute(sql`ALTER TABLE site_settings ADD COLUMN featured_event_id UUID REFERENCES events(id)`);
    } catch (e) { /* column may already exist */ }

    // Legal pages table for admin-editable legal content
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS legal_pages (
        id UUID PRIMARY KEY,
        slug VARCHAR(100) NOT NULL UNIQUE,
        title VARCHAR(255) NOT NULL,
        title_es VARCHAR(255),
        content_text TEXT NOT NULL,
        content_text_es TEXT,
        content_markdown TEXT NOT NULL,
        content_markdown_es TEXT,
        updated_at TIMESTAMP NOT NULL,
        updated_by UUID REFERENCES users(id),
        created_at TIMESTAMP NOT NULL
      )
    `);

    // FAQ questions table
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS faq_questions (
        id UUID PRIMARY KEY,
        question TEXT NOT NULL,
        question_es TEXT,
        answer TEXT NOT NULL,
        answer_es TEXT,
        enabled INTEGER NOT NULL DEFAULT 1,
        show_on_homepage INTEGER NOT NULL DEFAULT 0,
        rank INTEGER NOT NULL DEFAULT 0,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Legal settings table for legal page placeholder values
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS legal_settings (
        id UUID PRIMARY KEY,
        company_name VARCHAR(255),
        legal_entity_name VARCHAR(255),
        ruc_number VARCHAR(50),
        company_address TEXT,
        company_city VARCHAR(100),
        company_country VARCHAR(100),
        support_email VARCHAR(255),
        legal_email VARCHAR(255),
        governing_law VARCHAR(255),
        jurisdiction_city VARCHAR(100),
        updated_at TIMESTAMP NOT NULL,
        updated_by UUID REFERENCES users(id)
      )
    `);

    // ==================== Better Auth ====================
    // Better Auth core + admin plugin columns on the existing users table
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS image TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS banned BOOLEAN NOT NULL DEFAULT FALSE`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS ban_reason TEXT`);
    } catch (e) { /* column may already exist */ }
    try {
      await (db as any).execute(sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS ban_expires TIMESTAMP`);
    } catch (e) { /* column may already exist */ }

    // Better Auth sessions (replaces the legacy user_sessions table)
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS auth_sessions (
        id UUID PRIMARY KEY,
        user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        token VARCHAR(255) NOT NULL UNIQUE,
        expires_at TIMESTAMP NOT NULL,
        ip_address VARCHAR(45),
        user_agent TEXT,
        impersonated_by UUID,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Better Auth accounts: credential (password hash) and OAuth provider links
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS auth_accounts (
        id UUID PRIMARY KEY,
        user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
        account_id VARCHAR(255) NOT NULL,
        provider_id VARCHAR(100) NOT NULL,
        access_token TEXT,
        refresh_token TEXT,
        id_token TEXT,
        access_token_expires_at TIMESTAMP,
        refresh_token_expires_at TIMESTAMP,
        scope TEXT,
        password TEXT,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Better Auth verification values (magic links, password reset tokens)
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS auth_verifications (
        id UUID PRIMARY KEY,
        identifier VARCHAR(255) NOT NULL,
        value TEXT NOT NULL,
        expires_at TIMESTAMP NOT NULL,
        created_at TIMESTAMP NOT NULL,
        updated_at TIMESTAMP NOT NULL
      )
    `);

    // Better Auth rate limiting (used when Redis is not configured)
    await (db as any).execute(sql`
      CREATE TABLE IF NOT EXISTS auth_rate_limits (
        id VARCHAR(64) PRIMARY KEY,
        key VARCHAR(255),
        count BIGINT,
        last_request BIGINT
      )
    `);
  }

  // Indexes on foreign-key / hot-filter columns (CREATE INDEX IF NOT EXISTS works on both engines)
  const indexStatements = [
    `CREATE INDEX IF NOT EXISTS tickets_event_id_idx ON tickets(event_id)`,
    `CREATE INDEX IF NOT EXISTS tickets_user_id_idx ON tickets(user_id)`,
    `CREATE INDEX IF NOT EXISTS tickets_booking_id_idx ON tickets(booking_id)`,
    `CREATE INDEX IF NOT EXISTS tickets_status_idx ON tickets(status)`,
    `CREATE INDEX IF NOT EXISTS payments_ticket_id_idx ON payments(ticket_id)`,
    `CREATE INDEX IF NOT EXISTS payments_status_idx ON payments(status)`,
    `CREATE INDEX IF NOT EXISTS payments_source_idx ON payments(source)`,
    `CREATE INDEX IF NOT EXISTS idempotency_keys_created_at_idx ON idempotency_keys(created_at)`,
    `CREATE INDEX IF NOT EXISTS email_logs_event_id_idx ON email_logs(event_id)`,
    `CREATE INDEX IF NOT EXISTS magic_link_tokens_token_idx ON magic_link_tokens(token)`,
    `CREATE INDEX IF NOT EXISTS auth_sessions_user_id_idx ON auth_sessions(user_id)`,
    `CREATE INDEX IF NOT EXISTS auth_accounts_user_id_idx ON auth_accounts(user_id)`,
    `CREATE UNIQUE INDEX IF NOT EXISTS auth_accounts_provider_account_idx ON auth_accounts(provider_id, account_id)`,
    `CREATE INDEX IF NOT EXISTS auth_verifications_identifier_idx ON auth_verifications(identifier)`,
    `CREATE INDEX IF NOT EXISTS auth_rate_limits_key_idx ON auth_rate_limits(key)`,
  ];
  for (const stmt of indexStatements) {
    try {
      if (dbType === 'sqlite') {
        await (db as any).run(sql.raw(stmt));
      } else {
        await (db as any).execute(sql.raw(stmt));
      }
    } catch (e) { /* index may already exist */ }
  }

  // ==================== Better Auth data backfill ====================
  // Idempotent: every statement is guarded so re-running migrate is safe, and
  // legacy users are distinguished from Better-Auth-created users by having
  // users.password / users.google_id set (Better Auth never writes either).
  if (dbType === 'sqlite') {
    // Legacy password hashes -> credential accounts (argon2 and bcrypt hashes
    // both stay valid via the custom password verifier in lib/betterAuth.ts)
    await (db as any).run(sql`
      INSERT INTO auth_accounts (id, user_id, account_id, provider_id, password, created_at, updated_at)
      SELECT lower(hex(randomblob(16))), u.id, u.id, 'credential', u.password,
             CAST(strftime('%s','now') AS INTEGER) * 1000, CAST(strftime('%s','now') AS INTEGER) * 1000
      FROM users u
      WHERE u.password IS NOT NULL AND u.password != ''
        AND NOT EXISTS (
          SELECT 1 FROM auth_accounts a WHERE a.user_id = u.id AND a.provider_id = 'credential'
        )
    `);

    // Legacy Google links -> google provider accounts
    await (db as any).run(sql`
      INSERT INTO auth_accounts (id, user_id, account_id, provider_id, created_at, updated_at)
      SELECT lower(hex(randomblob(16))), u.id, u.google_id, 'google',
             CAST(strftime('%s','now') AS INTEGER) * 1000, CAST(strftime('%s','now') AS INTEGER) * 1000
      FROM users u
      WHERE u.google_id IS NOT NULL AND u.google_id != ''
        AND NOT EXISTS (
          SELECT 1 FROM auth_accounts a WHERE a.user_id = u.id AND a.provider_id = 'google'
        )
    `);

    // Claimed legacy accounts proved their email (register/claim link/Google)
    await (db as any).run(sql`
      UPDATE users SET email_verified = 1
      WHERE email_verified = 0 AND is_claimed = 1
        AND ((password IS NOT NULL AND password != '') OR google_id IS NOT NULL)
    `);

    // Suspended -> banned (admin plugin field); users.ts keeps them in sync
    await (db as any).run(sql`
      UPDATE users SET banned = 1, ban_reason = 'migrated: account suspended'
      WHERE account_status = 'suspended' AND banned = 0
    `);
  } else {
    await (db as any).execute(sql`
      INSERT INTO auth_accounts (id, user_id, account_id, provider_id, password, created_at, updated_at)
      SELECT gen_random_uuid(), u.id, u.id::text, 'credential', u.password, NOW(), NOW()
      FROM users u
      WHERE u.password IS NOT NULL AND u.password != ''
        AND NOT EXISTS (
          SELECT 1 FROM auth_accounts a WHERE a.user_id = u.id AND a.provider_id = 'credential'
        )
    `);

    await (db as any).execute(sql`
      INSERT INTO auth_accounts (id, user_id, account_id, provider_id, created_at, updated_at)
      SELECT gen_random_uuid(), u.id, u.google_id, 'google', NOW(), NOW()
      FROM users u
      WHERE u.google_id IS NOT NULL AND u.google_id != ''
        AND NOT EXISTS (
          SELECT 1 FROM auth_accounts a WHERE a.user_id = u.id AND a.provider_id = 'google'
        )
    `);

    await (db as any).execute(sql`
      UPDATE users SET email_verified = TRUE
      WHERE email_verified = FALSE AND is_claimed = 1
        AND ((password IS NOT NULL AND password != '') OR google_id IS NOT NULL)
    `);

    await (db as any).execute(sql`
      UPDATE users SET banned = TRUE, ban_reason = 'migrated: account suspended'
      WHERE account_status = 'suspended' AND banned = FALSE
    `);
  }

  // Backfill slugs for any events that don't have one yet (shared across DB types).
  // Ordered by creation so duplicate titles get deterministic -2, -3 suffixes.
  const allEvents = await dbAll<{ id: string; title: string; slug: string | null }>(
    (db as any).select().from(events).orderBy((events as any).createdAt)
  );
  const assignedSlugs: string[] = allEvents
    .filter((e) => e.slug)
    .map((e) => e.slug as string);
  let backfilled = 0;
  for (const ev of allEvents) {
    if (ev.slug) continue;
    const slug = uniqueSlug(ev.title || 'event', assignedSlugs);
    assignedSlugs.push(slug);
    await (db as any).update(events).set({ slug }).where(eq((events as any).id, ev.id));
    backfilled++;
  }
  if (backfilled > 0) {
    console.log(`Backfilled slugs for ${backfilled} event(s).`);
    // Bust the frontend cache so the homepage / sitemap pick up the new slugs
    // immediately instead of serving stale (pre-slug) data for up to the
    // revalidate window. Awaited (not fire-and-forget) so it runs before exit.
    const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3002';
    const secret = process.env.REVALIDATE_SECRET;
    if (secret) {
      try {
        const res = await fetch(`${frontendUrl}/api/revalidate`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ secret, tag: ['events-sitemap', 'next-event'] }),
        });
        console.log(res.ok ? 'Frontend cache revalidated.' : `Frontend revalidation returned ${res.status}.`);
      } catch (e: any) {
        console.warn('Frontend revalidation skipped (frontend not reachable):', e?.message || e);
      }
    }
  }

  console.log('Migrations completed successfully!');
  process.exit(0);
}

migrate().catch((err) => {
  console.error('Migration failed:', err);
  process.exit(1);
});
