// ─── Database Client Resolution ──────────────────────────────────────────────
// Cloud (Railway): uses @libsql/client for Turso remote databases
// Local (npm install): uses libsql (native SQLite binding) with async adapter
// This avoids shipping deprecated transitive deps (node-domexception) to npm users

let createClient;

// 1. Try @libsql/client (cloud — installed on Railway via nixpacks)
try {
  createClient = (await import('@libsql/client')).createClient;
} catch {
  // 2. Fallback: use libsql native binding with async adapter (local installs)
  try {
    const DatabaseMod = await import('libsql');
    const Database = DatabaseMod.default || DatabaseMod;
    createClient = function createLocalClient({ url }) {
      const dbPath = url.replace(/^file:/, '');
      const sqliteDb = new Database(dbPath);
      sqliteDb.pragma('journal_mode = WAL');
      return {
        execute: async (query) => {
          const sql = typeof query === 'string' ? query : query.sql;
          const args = typeof query === 'string' ? [] : (query.args || []);
          const stmt = sqliteDb.prepare(sql);
          if (sql.trim().match(/^(SELECT|PRAGMA|EXPLAIN)/i)) {
            const rows = args.length ? stmt.all(...args) : stmt.all();
            return { rows, columns: rows.length > 0 ? Object.keys(rows[0]) : [] };
          }
          const info = args.length ? stmt.run(...args) : stmt.run();
          return { rows: [], columns: [], rowsAffected: info.changes, lastInsertRowid: info.lastInsertRowid };
        },
        batch: async (queries) => {
          const results = [];
          for (const q of queries) {
            // Re-use the adapter's own execute method
            const sql = typeof q === 'string' ? q : q.sql;
            const args = typeof q === 'string' ? [] : (q.args || []);
            const stmt = sqliteDb.prepare(sql);
            if (sql.trim().match(/^(SELECT|PRAGMA|EXPLAIN)/i)) {
              const rows = args.length ? stmt.all(...args) : stmt.all();
              results.push({ rows, columns: rows.length > 0 ? Object.keys(rows[0]) : [] });
            } else {
              const info = args.length ? stmt.run(...args) : stmt.run();
              results.push({ rows: [], columns: [], rowsAffected: info.changes, lastInsertRowid: info.lastInsertRowid });
            }
          }
          return results;
        },
        close: () => sqliteDb.close(),
      };
    };
  } catch {
    // Neither available
  }
}

import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

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

// ANSI color codes
const colors = { reset: '\x1b[0m', bright: '\x1b[1m', cyan: '\x1b[36m', dim: '\x1b[2m' };
const c = {
  info: (t) => `${colors.cyan}${t}${colors.reset}`,
  bright: (t) => `${colors.bright}${t}${colors.reset}`,
  dim: (t) => `${colors.dim}${t}${colors.reset}`,
};

// Database URL resolution (lazy — resolved on first access)
let _db = null;
let _dbUrl = null;
let _dbAuthToken = null;

function resolveDbConfig() {
  if (_dbUrl) return;

  if (process.env.TURSO_DATABASE_URL) {
    _dbUrl = process.env.TURSO_DATABASE_URL.trim();
    _dbAuthToken = process.env.TURSO_AUTH_TOKEN?.trim();
    console.log(`${c.info('[DB]')} Using Turso: ${_dbUrl}`);
  } else if (process.env.DATABASE_PATH) {
    const dbPath = process.env.DATABASE_PATH.trim();
    try { if (!fs.existsSync(path.dirname(dbPath))) fs.mkdirSync(path.dirname(dbPath), { recursive: true }); } catch { /* Vercel read-only */ }
    _dbUrl = `file:${dbPath}`;
    console.log(`${c.info('[DB]')} Using custom path: ${dbPath}`);
  } else if (process.env.VERCEL) {
    _dbUrl = 'file:/tmp/auth.db';
    console.warn(`${c.info('[DB]')} WARNING: Ephemeral /tmp on Vercel. Set TURSO_DATABASE_URL for persistent data.`);
  } else {
    _dbUrl = `file:${path.join(__dirname, 'auth.db')}`;
    console.log(`${c.info('[DB]')} Using local: ${_dbUrl}`);
  }
}

function getDb() {
  if (!_db) {
    if (!createClient) {
      throw new Error('[DB] No database driver available. Install libsql or @libsql/client.');
    }
    resolveDbConfig();
    _db = createClient({ url: _dbUrl, ...(_dbAuthToken ? { authToken: _dbAuthToken } : {}) });
  }
  return _db;
}

// Lazy Proxy — defers createClient() until first DB call
const db = new Proxy({}, {
  get(_, prop) {
    const client = getDb();
    const val = client[prop];
    return typeof val === 'function' ? val.bind(client) : val;
  }
});

// ─── Schema ─────────────────────────────────────────────────────────────────────

const INIT_SQL = `
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    first_name TEXT,
    last_name TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_login DATETIME,
    is_active BOOLEAN DEFAULT 1,
    email TEXT,
    phone TEXT,
    git_name TEXT,
    git_email TEXT,
    has_completed_onboarding BOOLEAN DEFAULT 0,
    access_override TEXT DEFAULT NULL,
    user_code TEXT UNIQUE,
    google_id TEXT
);

CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
CREATE INDEX IF NOT EXISTS idx_users_user_code ON users(user_code);

CREATE TABLE IF NOT EXISTS api_keys (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    key_name TEXT NOT NULL,
    api_key TEXT UNIQUE NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_used DATETIME,
    is_active BOOLEAN DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key);
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);

CREATE TABLE IF NOT EXISTS user_credentials (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    credential_name TEXT NOT NULL,
    credential_type TEXT NOT NULL,
    credential_value TEXT NOT NULL,
    description TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);

CREATE TABLE IF NOT EXISTS relay_tokens (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    token TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL DEFAULT 'default',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_connected DATETIME,
    last_connected_ip TEXT,
    is_active BOOLEAN DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_relay_tokens_token ON relay_tokens(token);
CREATE INDEX IF NOT EXISTS idx_relay_tokens_user_id ON relay_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_relay_tokens_active ON relay_tokens(is_active);

CREATE TABLE IF NOT EXISTS subscriptions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    plan_id TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'active',
    amount INTEGER NOT NULL,
    currency TEXT NOT NULL DEFAULT 'INR',
    starts_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    expires_at DATETIME NOT NULL,
    razorpay_order_id TEXT,
    razorpay_payment_id TEXT,
    razorpay_signature TEXT,
    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_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
CREATE INDEX IF NOT EXISTS idx_subscriptions_expires ON subscriptions(expires_at);

CREATE TABLE IF NOT EXISTS payments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    subscription_id INTEGER,
    plan_id TEXT NOT NULL,
    amount INTEGER NOT NULL,
    currency TEXT NOT NULL DEFAULT 'INR',
    status TEXT NOT NULL DEFAULT 'pending',
    razorpay_order_id TEXT UNIQUE,
    razorpay_payment_id TEXT,
    razorpay_signature TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id);
CREATE INDEX IF NOT EXISTS idx_payments_order_id ON payments(razorpay_order_id);
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);

CREATE TABLE IF NOT EXISTS webhooks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    url TEXT NOT NULL,
    method TEXT NOT NULL DEFAULT 'POST',
    headers TEXT DEFAULT '{}',
    description TEXT,
    is_active BOOLEAN DEFAULT 1,
    last_triggered DATETIME,
    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_webhooks_user_id ON webhooks(user_id);
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active);

CREATE TABLE IF NOT EXISTS workflows (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    description TEXT,
    steps TEXT NOT NULL DEFAULT '[]',
    schedule TEXT DEFAULT NULL,
    schedule_enabled BOOLEAN DEFAULT 0,
    schedule_timezone TEXT DEFAULT 'UTC',
    is_active BOOLEAN DEFAULT 1,
    last_run DATETIME,
    next_run DATETIME,
    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_workflows_user_id ON workflows(user_id);
CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active);

CREATE TABLE IF NOT EXISTS workflow_runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    workflow_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    steps_completed INTEGER DEFAULT 0,
    total_steps INTEGER DEFAULT 0,
    result TEXT,
    error TEXT,
    started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    completed_at DATETIME,
    FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_id ON workflow_runs(workflow_id);
CREATE INDEX IF NOT EXISTS idx_workflow_runs_user_id ON workflow_runs(user_id);
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);

CREATE TABLE IF NOT EXISTS user_sandboxes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL UNIQUE,
    sandbox_path TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP,
    disk_usage_bytes INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT 1,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_user_sandboxes_user_id ON user_sandboxes(user_id);
CREATE INDEX IF NOT EXISTS idx_user_sandboxes_active ON user_sandboxes(is_active);

CREATE TABLE IF NOT EXISTS file_versions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    session_id TEXT NOT NULL,
    file_path TEXT NOT NULL,
    content TEXT NOT NULL,
    version INTEGER NOT NULL DEFAULT 1,
    action TEXT NOT NULL DEFAULT 'write',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_file_versions_session ON file_versions(session_id);
CREATE INDEX IF NOT EXISTS idx_file_versions_path ON file_versions(file_path, session_id);
CREATE INDEX IF NOT EXISTS idx_file_versions_user ON file_versions(user_id);

CREATE TABLE IF NOT EXISTS session_usage (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    session_id TEXT NOT NULL,
    provider TEXT NOT NULL DEFAULT 'claude',
    prompt_tokens INTEGER NOT NULL DEFAULT 0,
    completion_tokens INTEGER NOT NULL DEFAULT 0,
    cost_cents REAL NOT NULL DEFAULT 0.0,
    model TEXT,
    message_count INTEGER NOT NULL DEFAULT 0,
    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_session_usage_user ON session_usage(user_id);
CREATE INDEX IF NOT EXISTS idx_session_usage_session ON session_usage(session_id);

CREATE TABLE IF NOT EXISTS project_canvases (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    project_name TEXT NOT NULL,
    elements TEXT DEFAULT '[]',
    app_state TEXT DEFAULT '{}',
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE(user_id, project_name)
);

CREATE INDEX IF NOT EXISTS idx_project_canvases_user ON project_canvases(user_id);

CREATE TABLE IF NOT EXISTS user_projects (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    project_name TEXT NOT NULL,
    original_path TEXT NOT NULL,
    display_name TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE(user_id, original_path)
);

CREATE INDEX IF NOT EXISTS idx_user_projects_user_id ON user_projects(user_id);

CREATE TABLE IF NOT EXISTS password_reset_tokens (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    token TEXT UNIQUE NOT NULL,
    expires_at DATETIME NOT NULL,
    used BOOLEAN DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token ON password_reset_tokens(token);
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON password_reset_tokens(user_id);

CREATE TABLE IF NOT EXISTS active_connections (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id TEXT NOT NULL,
    project_name TEXT,
    connection_type TEXT DEFAULT 'relay',
    sandbox_id TEXT,
    last_cwd TEXT,
    last_machine TEXT,
    last_platform TEXT,
    connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    disconnected_at DATETIME,
    UNIQUE(user_id, connection_type)
);

CREATE INDEX IF NOT EXISTS idx_active_connections_user ON active_connections(user_id);

CREATE TABLE IF NOT EXISTS voice_calls (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    vapi_call_id TEXT,
    session_id TEXT,
    call_type TEXT DEFAULT 'voice',
    status TEXT DEFAULT 'in_progress',
    started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    ended_at DATETIME,
    duration_seconds INTEGER,
    ended_reason TEXT,
    cost REAL,
    transcript TEXT,
    summary TEXT,
    tools_used TEXT,
    messages_count 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_voice_calls_user ON voice_calls(user_id);
CREATE INDEX IF NOT EXISTS idx_voice_calls_vapi ON voice_calls(vapi_call_id);
`;

// ─── Migrations ─────────────────────────────────────────────────────────────────

const runMigrations = async () => {
  try {
    const result = await db.execute("PRAGMA table_info(users)");
    const cols = result.rows.map(r => r.name);

    const addCol = async (col, def) => {
      if (!cols.includes(col)) {
        await db.execute(`ALTER TABLE users ADD COLUMN ${col} ${def}`);
      }
    };

    await addCol('git_name', 'TEXT');
    await addCol('git_email', 'TEXT');
    await addCol('has_completed_onboarding', 'BOOLEAN DEFAULT 0');
    await addCol('email', 'TEXT');
    await addCol('phone', 'TEXT');
    await addCol('first_name', 'TEXT');
    await addCol('last_name', 'TEXT');
    await addCol('access_override', 'TEXT DEFAULT NULL');
    await addCol('user_code', 'TEXT');
    await addCol('google_id', 'TEXT');
    try { await db.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_user_code ON users(user_code)'); } catch { /* ignore */ }

    // Backfill user_code (upc-001, upc-002, ...) for users missing it
    try {
      const noCode = await db.execute("SELECT id FROM users WHERE user_code IS NULL ORDER BY id ASC");
      for (const row of noCode.rows) {
        const code = `upc-${String(row.id).padStart(3, '0')}`;
        await db.execute({ sql: 'UPDATE users SET user_code = ? WHERE id = ?', args: [code, row.id] });
      }
      if (noCode.rows.length > 0) console.log(`${c.info('[DB]')} Assigned user_code to ${noCode.rows.length} users`);
    } catch (e) { console.warn('[DB] user_code backfill:', e.message); }

    // Migrate old ck_ API keys → up-cli- prefix
    try {
      const migrated = await db.execute("UPDATE api_keys SET api_key = 'up-cli-' || SUBSTR(api_key, 4) WHERE api_key LIKE 'ck_%'");
      if (migrated.rowsAffected > 0) console.log(`${c.info('[DB]')} Migrated ${migrated.rowsAffected} API keys: ck_ → up-cli-`);
    } catch { /* empty table or already migrated */ }

    // Backfill: ensure every user has records in all tables (relay_tokens, api_keys, subscriptions, payments, access_override)
    try {
      const allUsers = await db.execute('SELECT id, username, access_override FROM users WHERE is_active = 1');
      let backfilled = 0;
      for (const user of allUsers.rows) {
        // Relay token
        const tokens = await db.execute({ sql: 'SELECT id FROM relay_tokens WHERE user_id = ? LIMIT 1', args: [user.id] });
        if (tokens.rows.length === 0) {
          const token = 'upfyn_' + crypto.randomBytes(32).toString('hex');
          await db.execute({ sql: 'INSERT INTO relay_tokens (user_id, token, name) VALUES (?, ?, ?)', args: [user.id, token, 'default'] });
          backfilled++;
        }
        // API key
        const keys = await db.execute({ sql: 'SELECT id FROM api_keys WHERE user_id = ? LIMIT 1', args: [user.id] });
        if (keys.rows.length === 0) {
          const apiKey = 'up-cli-' + crypto.randomBytes(32).toString('hex');
          await db.execute({ sql: 'INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)', args: [user.id, 'default', apiKey] });
          backfilled++;
        }
        // Yearly subscription (if none exists)
        const subs = await db.execute({ sql: 'SELECT id FROM subscriptions WHERE user_id = ? LIMIT 1', args: [user.id] });
        if (subs.rows.length === 0) {
          const now = new Date();
          const expiresAt = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000).toISOString();
          const subResult = await db.execute({
            sql: `INSERT INTO subscriptions (user_id, plan_id, status, amount, currency, starts_at, expires_at) VALUES (?, 'yearly', 'active', 49900, 'INR', ?, ?)`,
            args: [user.id, now.toISOString(), expiresAt]
          });
          // Payment record for the subscription
          const subId = Number(subResult.lastInsertRowid);
          await db.execute({
            sql: `INSERT INTO payments (user_id, subscription_id, plan_id, amount, currency, status, razorpay_order_id) VALUES (?, ?, 'yearly', 49900, 'INR', 'paid', ?)`,
            args: [user.id, subId, `seed_order_${user.id}_${Date.now()}`]
          });
          backfilled++;
        }
        // Set access_override = 'paid' if not already set
        if (!user.access_override || user.access_override !== 'paid') {
          await db.execute({ sql: "UPDATE users SET access_override = 'paid' WHERE id = ?", args: [user.id] });
          backfilled++;
        }
      }
      if (backfilled > 0) console.log(`${c.info('[DB]')} Backfilled ${backfilled} records for existing users`);
    } catch (e) { console.warn('[DB] Backfill warning:', e.message); }

    // Add last_connected_ip column to relay_tokens if missing
    try {
      const rtCols = await db.execute("PRAGMA table_info(relay_tokens)");
      const rtColNames = rtCols.rows.map(r => r.name);
      if (!rtColNames.includes('last_connected_ip')) {
        await db.execute('ALTER TABLE relay_tokens ADD COLUMN last_connected_ip TEXT');
      }
    } catch { /* column may already exist */ }

    console.log(`${c.info('[DB]')} Migrations complete`);
  } catch (error) {
    console.error('Migration error:', error.message);
    throw error;
  }
};

// ─── Initialize ─────────────────────────────────────────────────────────────────

const initializeDatabase = async () => {
  try {
    try { await db.execute('PRAGMA foreign_keys = ON'); } catch (e) { console.warn('[DB] PRAGMA foreign_keys not supported:', e.message); }

    const stmts = INIT_SQL.split(';').map(s => s.trim()).filter(s => s.length > 0 && !s.startsWith('--'));
    for (const stmt of stmts) {
      try { await db.execute(stmt); } catch (e) { console.error('[DB] Statement failed:', stmt.substring(0, 80), e.message); throw e; }
    }

    console.log(`${c.info('[DB]')} Initialized`);
    await runMigrations();
  } catch (error) {
    console.error('DB init error:', error.message);
    throw error;
  }
};

// ─── Helpers ────────────────────────────────────────────────────────────────────

const getRow = (result) => result.rows.length > 0 ? result.rows[0] : null;
const ensureForeignKeys = async () => { try { await db.execute('PRAGMA foreign_keys = ON'); } catch { /* Turso */ } };

// ─── User DB ────────────────────────────────────────────────────────────────────

const userDb = {
  hasUsers: async () => {
    const result = await db.execute('SELECT COUNT(*) as count FROM users');
    return Number(getRow(result).count) > 0;
  },

  createUser: async (username, passwordHash, email = null, phone = null, firstName = null, lastName = null) => {
    const result = await db.execute({
      sql: 'INSERT INTO users (username, password_hash, email, phone, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?)',
      args: [username, passwordHash, email, phone, firstName, lastName]
    });
    const userId = Number(result.lastInsertRowid);
    // Auto-assign user_code (upc-001, upc-002, ...)
    const userCode = `upc-${String(userId).padStart(3, '0')}`;
    try { await db.execute({ sql: 'UPDATE users SET user_code = ? WHERE id = ?', args: [userCode, userId] }); } catch { /* non-critical */ }
    return { id: userId, username, first_name: firstName, last_name: lastName, user_code: userCode };
  },

  getUserByUsername: async (username) => {
    const lower = (username || '').toLowerCase();
    const result = await db.execute({
      sql: 'SELECT * FROM users WHERE (LOWER(username) = ? OR LOWER(email) = ? OR phone = ? OR LOWER(user_code) = ?) AND is_active = 1',
      args: [lower, lower, username, lower]
    });
    return getRow(result);
  },

  updateLastLogin: async (userId) => {
    await db.execute({ sql: 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', args: [userId] });
  },

  getUserById: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT id, username, first_name, last_name, email, phone, created_at, last_login, access_override, user_code FROM users WHERE id = ? AND is_active = 1',
      args: [userId]
    });
    return getRow(result);
  },

  getFirstUser: async () => {
    const result = await db.execute('SELECT id, username, first_name, last_name, email, phone, created_at, last_login, access_override, user_code FROM users WHERE is_active = 1 LIMIT 1');
    return getRow(result);
  },

  updateGitConfig: async (userId, gitName, gitEmail) => {
    await db.execute({ sql: 'UPDATE users SET git_name = ?, git_email = ? WHERE id = ?', args: [gitName, gitEmail, userId] });
  },

  getGitConfig: async (userId) => {
    const result = await db.execute({ sql: 'SELECT git_name, git_email FROM users WHERE id = ?', args: [userId] });
    return getRow(result);
  },

  completeOnboarding: async (userId) => {
    await db.execute({ sql: 'UPDATE users SET has_completed_onboarding = 1 WHERE id = ?', args: [userId] });
  },

  hasCompletedOnboarding: async (userId) => {
    const result = await db.execute({ sql: 'SELECT has_completed_onboarding FROM users WHERE id = ?', args: [userId] });
    const row = getRow(result);
    return row?.has_completed_onboarding === 1;
  },

  setAccessOverride: async (userId, value) => {
    await db.execute({
      sql: 'UPDATE users SET access_override = ? WHERE id = ?',
      args: [value, userId]
    });
  },

  getUserByGoogleId: async (googleId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM users WHERE google_id = ? AND is_active = 1',
      args: [googleId]
    });
    return getRow(result);
  },

  setUserGoogleId: async (userId, googleId) => {
    await db.execute({
      sql: 'UPDATE users SET google_id = ? WHERE id = ?',
      args: [googleId, userId]
    });
  },

  getUserByEmail: async (email) => {
    const result = await db.execute({
      sql: 'SELECT * FROM users WHERE LOWER(email) = ? AND is_active = 1',
      args: [(email || '').toLowerCase()]
    });
    return getRow(result);
  },

  updatePasswordHash: async (userId, passwordHash) => {
    await db.execute({
      sql: 'UPDATE users SET password_hash = ? WHERE id = ?',
      args: [passwordHash, userId]
    });
  }
};

// ─── API Keys DB ────────────────────────────────────────────────────────────────

const apiKeysDb = {
  generateApiKey: () => 'up-cli-' + crypto.randomBytes(32).toString('hex'),

  createApiKey: async (userId, keyName) => {
    const apiKey = apiKeysDb.generateApiKey();
    const result = await db.execute({
      sql: 'INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)',
      args: [userId, keyName, apiKey]
    });
    return { id: Number(result.lastInsertRowid), keyName, apiKey };
  },

  getApiKeys: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC',
      args: [userId]
    });
    return result.rows;
  },

  validateApiKey: async (apiKey) => {
    const result = await db.execute({
      sql: `SELECT u.id, u.username, ak.id as api_key_id
            FROM api_keys ak JOIN users u ON ak.user_id = u.id
            WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1`,
      args: [apiKey]
    });
    const row = getRow(result);
    if (row) {
      await db.execute({ sql: 'UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?', args: [row.api_key_id] });
    }
    return row;
  },

  deleteApiKey: async (userId, apiKeyId) => {
    await ensureForeignKeys();
    const result = await db.execute({ sql: 'DELETE FROM api_keys WHERE id = ? AND user_id = ?', args: [apiKeyId, userId] });
    return result.rowsAffected > 0;
  },

  toggleApiKey: async (userId, apiKeyId, isActive) => {
    const result = await db.execute({
      sql: 'UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?',
      args: [isActive ? 1 : 0, apiKeyId, userId]
    });
    return result.rowsAffected > 0;
  },

  deactivateAll: async (userId) => {
    await db.execute({ sql: 'UPDATE api_keys SET is_active = 0 WHERE user_id = ?', args: [userId] });
  }
};

// ─── Credentials DB ─────────────────────────────────────────────────────────────

const credentialsDb = {
  createCredential: async (userId, credentialName, credentialType, credentialValue, description = null) => {
    const result = await db.execute({
      sql: 'INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)',
      args: [userId, credentialName, credentialType, credentialValue, description]
    });
    return { id: Number(result.lastInsertRowid), credentialName, credentialType };
  },

  getCredentials: async (userId, credentialType = null) => {
    let sql = 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ?';
    const args = [userId];
    if (credentialType) { sql += ' AND credential_type = ?'; args.push(credentialType); }
    sql += ' ORDER BY created_at DESC';
    return (await db.execute({ sql, args })).rows;
  },

  getActiveCredential: async (userId, credentialType) => {
    const result = await db.execute({
      sql: 'SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1',
      args: [userId, credentialType]
    });
    return getRow(result)?.credential_value || null;
  },

  deleteCredential: async (userId, credentialId) => {
    await ensureForeignKeys();
    const result = await db.execute({ sql: 'DELETE FROM user_credentials WHERE id = ? AND user_id = ?', args: [credentialId, userId] });
    return result.rowsAffected > 0;
  },

  toggleCredential: async (userId, credentialId, isActive) => {
    const result = await db.execute({
      sql: 'UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?',
      args: [isActive ? 1 : 0, credentialId, userId]
    });
    return result.rowsAffected > 0;
  }
};

// Backward compat
const githubTokensDb = {
  createGithubToken: (userId, tokenName, githubToken, description = null) => credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description),
  getGithubTokens: (userId) => credentialsDb.getCredentials(userId, 'github_token'),
  getActiveGithubToken: (userId) => credentialsDb.getActiveCredential(userId, 'github_token'),
  deleteGithubToken: (userId, tokenId) => credentialsDb.deleteCredential(userId, tokenId),
  toggleGithubToken: (userId, tokenId, isActive) => credentialsDb.toggleCredential(userId, tokenId, isActive)
};

// ─── Plan Durations ─────────────────────────────────────────────────────────────

const PLAN_DURATIONS = {
  monthly: 30,
  'half-yearly': 180,
  yearly: 365,
};

// ─── Subscription DB ────────────────────────────────────────────────────────────

const subscriptionDb = {
  getActiveSub: async (userId) => {
    const result = await db.execute({
      sql: `SELECT * FROM subscriptions WHERE user_id = ? AND status = 'active' AND expires_at > CURRENT_TIMESTAMP ORDER BY expires_at DESC LIMIT 1`,
      args: [userId]
    });
    return getRow(result);
  },

  getAllSubs: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC',
      args: [userId]
    });
    return result.rows;
  },

  createSub: async (userId, planId, amount, currency, razorpayOrderId, razorpayPaymentId, razorpaySignature) => {
    const days = PLAN_DURATIONS[planId] || 30;

    // Check if user has an active sub — extend from its expiry, otherwise start now
    const existing = await subscriptionDb.getActiveSub(userId);
    const startsAt = existing ? existing.expires_at : new Date().toISOString();
    const startsDate = new Date(startsAt);
    const expiresAt = new Date(startsDate.getTime() + days * 24 * 60 * 60 * 1000).toISOString();

    const result = await db.execute({
      sql: `INSERT INTO subscriptions (user_id, plan_id, status, amount, currency, starts_at, expires_at, razorpay_order_id, razorpay_payment_id, razorpay_signature) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`,
      args: [userId, planId, amount, currency, startsAt, expiresAt, razorpayOrderId, razorpayPaymentId, razorpaySignature]
    });
    return { id: Number(result.lastInsertRowid), planId, status: 'active', startsAt, expiresAt };
  },

  cancelSub: async (userId, subId) => {
    const result = await db.execute({
      sql: `UPDATE subscriptions SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`,
      args: [subId, userId]
    });
    return result.rowsAffected > 0;
  },

  expireOverdue: async () => {
    const result = await db.execute(
      `UPDATE subscriptions SET status = 'expired', updated_at = CURRENT_TIMESTAMP WHERE status = 'active' AND expires_at <= CURRENT_TIMESTAMP`
    );
    return result.rowsAffected;
  },

  // Cancel current active sub and create a new one starting now (used for downgrades)
  changePlan: async (userId, newPlanId, amount, currency, razorpayOrderId, razorpayPaymentId, razorpaySignature) => {
    // Cancel existing active subscription
    const existing = await subscriptionDb.getActiveSub(userId);
    if (existing) {
      await db.execute({
        sql: `UPDATE subscriptions SET status = 'changed', updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`,
        args: [existing.id, userId]
      });
    }

    // Create new subscription starting now (not extending from old expiry)
    const days = PLAN_DURATIONS[newPlanId] || 30;
    const startsAt = new Date().toISOString();
    const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();

    const result = await db.execute({
      sql: `INSERT INTO subscriptions (user_id, plan_id, status, amount, currency, starts_at, expires_at, razorpay_order_id, razorpay_payment_id, razorpay_signature) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`,
      args: [userId, newPlanId, amount, currency, startsAt, expiresAt, razorpayOrderId, razorpayPaymentId, razorpaySignature]
    });
    return { id: Number(result.lastInsertRowid), planId: newPlanId, status: 'active', startsAt, expiresAt };
  },
};

// ─── Payment DB ─────────────────────────────────────────────────────────────────

const paymentDb = {
  createPayment: async (userId, planId, amount, currency, razorpayOrderId) => {
    const result = await db.execute({
      sql: `INSERT INTO payments (user_id, plan_id, amount, currency, razorpay_order_id, status) VALUES (?, ?, ?, ?, ?, 'pending')`,
      args: [userId, planId, amount, currency, razorpayOrderId]
    });
    return { id: Number(result.lastInsertRowid), razorpayOrderId };
  },

  getByOrderId: async (razorpayOrderId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM payments WHERE razorpay_order_id = ?',
      args: [razorpayOrderId]
    });
    return getRow(result);
  },

  markPaid: async (razorpayOrderId, razorpayPaymentId, razorpaySignature, subscriptionId) => {
    await db.execute({
      sql: `UPDATE payments SET status = 'paid', razorpay_payment_id = ?, razorpay_signature = ?, subscription_id = ? WHERE razorpay_order_id = ?`,
      args: [razorpayPaymentId, razorpaySignature, subscriptionId, razorpayOrderId]
    });
  },

  markFailed: async (razorpayOrderId) => {
    await db.execute({
      sql: `UPDATE payments SET status = 'failed' WHERE razorpay_order_id = ?`,
      args: [razorpayOrderId]
    });
  },

  getUserPayments: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM payments WHERE user_id = ? ORDER BY created_at DESC',
      args: [userId]
    });
    return result.rows;
  },
};

// ─── Relay Tokens DB ────────────────────────────────────────────────────────────

const relayTokensDb = {
  generateToken: () => 'upfyn_' + crypto.randomBytes(32).toString('hex'),

  createToken: async (userId, name = 'default') => {
    const token = relayTokensDb.generateToken();
    await db.execute({ sql: 'INSERT INTO relay_tokens (user_id, token, name) VALUES (?, ?, ?)', args: [userId, token, name] });
    return { token, name };
  },

  validateToken: async (token, clientIp = null) => {
    try {
      const result = await db.execute({
        sql: `SELECT rt.id, rt.user_id, rt.name, u.id as uid, u.username
              FROM relay_tokens rt JOIN users u ON rt.user_id = u.id
              WHERE rt.token = ? AND rt.is_active = 1 AND u.is_active = 1`,
        args: [token]
      });
      const row = getRow(result);
      if (row) {
        const updateSql = clientIp
          ? 'UPDATE relay_tokens SET last_connected = CURRENT_TIMESTAMP, last_connected_ip = ? WHERE id = ?'
          : 'UPDATE relay_tokens SET last_connected = CURRENT_TIMESTAMP WHERE id = ?';
        const updateArgs = clientIp ? [clientIp, row.id] : [row.id];
        await db.execute({ sql: updateSql, args: updateArgs });
      }
      return row;
    } catch { return null; }
  },

  getTokens: async (userId) => {
    return (await db.execute({ sql: 'SELECT id, name, token, created_at, last_connected, is_active FROM relay_tokens WHERE user_id = ? ORDER BY created_at DESC', args: [userId] })).rows;
  },

  deleteToken: async (userId, tokenId) => {
    await ensureForeignKeys();
    return (await db.execute({ sql: 'DELETE FROM relay_tokens WHERE id = ? AND user_id = ?', args: [tokenId, userId] })).rowsAffected > 0;
  },

  toggleToken: async (userId, tokenId, isActive) => {
    return (await db.execute({ sql: 'UPDATE relay_tokens SET is_active = ? WHERE id = ? AND user_id = ?', args: [isActive ? 1 : 0, tokenId, userId] })).rowsAffected > 0;
  },

  deactivateAll: async (userId) => {
    await db.execute({ sql: 'UPDATE relay_tokens SET is_active = 0 WHERE user_id = ?', args: [userId] });
  }
};

// ─── Webhook DB ──────────────────────────────────────────────────────────────

const webhookDb = {
  getAll: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM webhooks WHERE user_id = ? ORDER BY created_at DESC',
      args: [userId]
    });
    return result.rows;
  },

  getById: async (id, userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM webhooks WHERE id = ? AND user_id = ?',
      args: [id, userId]
    });
    return getRow(result);
  },

  create: async (userId, { name, url, method, headers, description }) => {
    const result = await db.execute({
      sql: 'INSERT INTO webhooks (user_id, name, url, method, headers, description) VALUES (?, ?, ?, ?, ?, ?)',
      args: [userId, name, url, method || 'POST', headers || '{}', description || null]
    });
    return { id: Number(result.lastInsertRowid), name, url, method: method || 'POST' };
  },

  update: async (id, userId, { name, url, method, headers, description }) => {
    const result = await db.execute({
      sql: 'UPDATE webhooks SET name = ?, url = ?, method = ?, headers = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
      args: [name, url, method, headers || '{}', description || null, id, userId]
    });
    return result.rowsAffected > 0;
  },

  delete: async (id, userId) => {
    await ensureForeignKeys();
    const result = await db.execute({
      sql: 'DELETE FROM webhooks WHERE id = ? AND user_id = ?',
      args: [id, userId]
    });
    return result.rowsAffected > 0;
  },

  updateLastTriggered: async (id) => {
    await db.execute({
      sql: 'UPDATE webhooks SET last_triggered = CURRENT_TIMESTAMP WHERE id = ?',
      args: [id]
    });
  }
};

// ─── Workflow DB ─────────────────────────────────────────────────────────────

const workflowDb = {
  getAll: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM workflows WHERE user_id = ? ORDER BY created_at DESC',
      args: [userId]
    });
    return result.rows;
  },

  getById: async (id, userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM workflows WHERE id = ? AND user_id = ?',
      args: [id, userId]
    });
    return getRow(result);
  },

  create: async (userId, { name, description, steps, schedule, schedule_enabled, schedule_timezone }) => {
    const result = await db.execute({
      sql: 'INSERT INTO workflows (user_id, name, description, steps, schedule, schedule_enabled, schedule_timezone) VALUES (?, ?, ?, ?, ?, ?, ?)',
      args: [userId, name, description || null, JSON.stringify(steps || []), schedule || null, schedule_enabled ? 1 : 0, schedule_timezone || 'UTC']
    });
    return { id: Number(result.lastInsertRowid), name };
  },

  update: async (id, userId, { name, description, steps, schedule, schedule_enabled, schedule_timezone }) => {
    const result = await db.execute({
      sql: 'UPDATE workflows SET name = ?, description = ?, steps = ?, schedule = ?, schedule_enabled = ?, schedule_timezone = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
      args: [name, description || null, JSON.stringify(steps || []), schedule || null, schedule_enabled ? 1 : 0, schedule_timezone || 'UTC', id, userId]
    });
    return result.rowsAffected > 0;
  },

  delete: async (id, userId) => {
    await ensureForeignKeys();
    const result = await db.execute({
      sql: 'DELETE FROM workflows WHERE id = ? AND user_id = ?',
      args: [id, userId]
    });
    return result.rowsAffected > 0;
  },

  updateLastRun: async (id) => {
    await db.execute({
      sql: 'UPDATE workflows SET last_run = CURRENT_TIMESTAMP WHERE id = ?',
      args: [id]
    });
  },

  getScheduled: async () => {
    const result = await db.execute(
      'SELECT * FROM workflows WHERE schedule IS NOT NULL AND schedule_enabled = 1 AND is_active = 1'
    );
    return result.rows;
  },

  updateNextRun: async (id, nextRun) => {
    await db.execute({
      sql: 'UPDATE workflows SET next_run = ? WHERE id = ?',
      args: [nextRun, id]
    });
  },

  createRun: async (workflowId, userId, totalSteps) => {
    const result = await db.execute({
      sql: 'INSERT INTO workflow_runs (workflow_id, user_id, status, total_steps) VALUES (?, ?, ?, ?)',
      args: [workflowId, userId, 'running', totalSteps]
    });
    return { id: Number(result.lastInsertRowid) };
  },

  updateRun: async (runId, { status, stepsCompleted, result: runResult, error }) => {
    const sets = ['status = ?'];
    const args = [status];
    if (stepsCompleted !== undefined) { sets.push('steps_completed = ?'); args.push(stepsCompleted); }
    if (runResult !== undefined) { sets.push('result = ?'); args.push(typeof runResult === 'string' ? runResult : JSON.stringify(runResult)); }
    if (error !== undefined) { sets.push('error = ?'); args.push(error); }
    if (status === 'completed' || status === 'failed') { sets.push('completed_at = CURRENT_TIMESTAMP'); }
    args.push(runId);
    await db.execute({ sql: `UPDATE workflow_runs SET ${sets.join(', ')} WHERE id = ?`, args });
  },

  getRuns: async (workflowId, userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM workflow_runs WHERE workflow_id = ? AND user_id = ? ORDER BY started_at DESC LIMIT 20',
      args: [workflowId, userId]
    });
    return result.rows;
  }
};

// ─── File Version History (opencode pattern: files table for undo/redo) ───────

const fileVersionDb = {
  save: async (userId, sessionId, filePath, content, action = 'write') => {
    // Get next version number for this file+session
    const existing = await db.execute({
      sql: 'SELECT MAX(version) as maxVer FROM file_versions WHERE session_id = ? AND file_path = ?',
      args: [sessionId, filePath]
    });
    const nextVersion = (existing.rows[0]?.maxVer || 0) + 1;
    await db.execute({
      sql: 'INSERT INTO file_versions (user_id, session_id, file_path, content, version, action) VALUES (?, ?, ?, ?, ?, ?)',
      args: [userId, sessionId, filePath, content, nextVersion, action]
    });
    return nextVersion;
  },

  getVersions: async (sessionId, filePath, limit = 50) => {
    const result = await db.execute({
      sql: 'SELECT id, version, action, created_at FROM file_versions WHERE session_id = ? AND file_path = ? ORDER BY version DESC LIMIT ?',
      args: [sessionId, filePath, limit]
    });
    return result.rows;
  },

  getVersion: async (sessionId, filePath, version) => {
    const result = await db.execute({
      sql: 'SELECT * FROM file_versions WHERE session_id = ? AND file_path = ? AND version = ?',
      args: [sessionId, filePath, version]
    });
    return result.rows[0] || null;
  },

  getLatest: async (sessionId, filePath) => {
    const result = await db.execute({
      sql: 'SELECT * FROM file_versions WHERE session_id = ? AND file_path = ? ORDER BY version DESC LIMIT 1',
      args: [sessionId, filePath]
    });
    return result.rows[0] || null;
  },

  getSessionFiles: async (sessionId) => {
    const result = await db.execute({
      sql: 'SELECT DISTINCT file_path, MAX(version) as latest_version, COUNT(*) as total_versions FROM file_versions WHERE session_id = ? GROUP BY file_path',
      args: [sessionId]
    });
    return result.rows;
  },
};

// ─── Session Usage / Cost Tracking (opencode pattern: per-session token+cost tracking) ───

const sessionUsageDb = {
  upsert: async (userId, sessionId, { provider, promptTokens, completionTokens, costCents, model }) => {
    // Try update first, then insert
    const existing = await db.execute({
      sql: 'SELECT id FROM session_usage WHERE session_id = ? AND user_id = ?',
      args: [sessionId, userId]
    });
    if (existing.rows.length > 0) {
      await db.execute({
        sql: `UPDATE session_usage SET
          prompt_tokens = prompt_tokens + ?,
          completion_tokens = completion_tokens + ?,
          cost_cents = cost_cents + ?,
          message_count = message_count + 1,
          model = COALESCE(?, model),
          updated_at = CURRENT_TIMESTAMP
          WHERE session_id = ? AND user_id = ?`,
        args: [promptTokens || 0, completionTokens || 0, costCents || 0, model, sessionId, userId]
      });
    } else {
      await db.execute({
        sql: 'INSERT INTO session_usage (user_id, session_id, provider, prompt_tokens, completion_tokens, cost_cents, model, message_count) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
        args: [userId, sessionId, provider || 'claude', promptTokens || 0, completionTokens || 0, costCents || 0, model]
      });
    }
  },

  getSession: async (sessionId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM session_usage WHERE session_id = ?',
      args: [sessionId]
    });
    return result.rows[0] || null;
  },

  getUserUsage: async (userId, days = 30) => {
    const result = await db.execute({
      sql: `SELECT
        SUM(prompt_tokens) as total_prompt_tokens,
        SUM(completion_tokens) as total_completion_tokens,
        SUM(cost_cents) as total_cost_cents,
        SUM(message_count) as total_messages,
        COUNT(*) as total_sessions
      FROM session_usage
      WHERE user_id = ? AND created_at >= datetime('now', '-' || ? || ' days')`,
      args: [userId, days]
    });
    return result.rows[0] || null;
  },

  getUserSessions: async (userId, limit = 20) => {
    const result = await db.execute({
      sql: 'SELECT * FROM session_usage WHERE user_id = ? ORDER BY updated_at DESC LIMIT ?',
      args: [userId, limit]
    });
    return result.rows;
  },
};

// ─── Active Connections DB ────────────────────────────────────────────────────────

const connectionDb = {
  connect: async (userId, projectName, type = 'relay', sandboxId = null, extra = {}) => {
    const { cwd, machine, platform } = extra;
    // Upsert: if user already has this connection type, update it
    const existing = await db.execute({
      sql: 'SELECT id FROM active_connections WHERE user_id = ? AND connection_type = ?',
      args: [String(userId), type]
    });
    if (existing.rows.length > 0) {
      await db.execute({
        sql: 'UPDATE active_connections SET project_name = ?, sandbox_id = ?, last_cwd = COALESCE(?, last_cwd), last_machine = COALESCE(?, last_machine), last_platform = COALESCE(?, last_platform), connected_at = CURRENT_TIMESTAMP, disconnected_at = NULL WHERE user_id = ? AND connection_type = ?',
        args: [projectName, sandboxId, cwd, machine, platform, String(userId), type]
      });
    } else {
      await db.execute({
        sql: 'INSERT INTO active_connections (user_id, project_name, connection_type, sandbox_id, last_cwd, last_machine, last_platform) VALUES (?, ?, ?, ?, ?, ?, ?)',
        args: [String(userId), projectName, type, sandboxId, cwd, machine, platform]
      });
    }
  },

  disconnect: async (userId, type = 'relay') => {
    await db.execute({
      sql: 'UPDATE active_connections SET disconnected_at = CURRENT_TIMESTAMP WHERE user_id = ? AND connection_type = ? AND disconnected_at IS NULL',
      args: [String(userId), type]
    });
  },

  getActive: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM active_connections WHERE user_id = ? AND disconnected_at IS NULL',
      args: [String(userId)]
    });
    return result.rows;
  },

  getAllActive: async () => {
    const result = await db.execute({
      sql: 'SELECT * FROM active_connections WHERE disconnected_at IS NULL ORDER BY connected_at DESC'
    });
    return result.rows;
  },
};

// ─── Canvas DB ───────────────────────────────────────────────────────────────────

const canvasDb = {
  save: async (userId, projectName, elements, appState) => {
    const elementsJson = typeof elements === 'string' ? elements : JSON.stringify(elements);
    const appStateJson = typeof appState === 'string' ? appState : JSON.stringify(appState);
    const existing = await db.execute({
      sql: 'SELECT id FROM project_canvases WHERE user_id = ? AND project_name = ?',
      args: [userId, projectName]
    });
    if (existing.rows.length > 0) {
      await db.execute({
        sql: 'UPDATE project_canvases SET elements = ?, app_state = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND project_name = ?',
        args: [elementsJson, appStateJson, userId, projectName]
      });
    } else {
      await db.execute({
        sql: 'INSERT INTO project_canvases (user_id, project_name, elements, app_state) VALUES (?, ?, ?, ?)',
        args: [userId, projectName, elementsJson, appStateJson]
      });
    }
  },

  load: async (userId, projectName) => {
    const result = await db.execute({
      sql: 'SELECT elements, app_state, updated_at FROM project_canvases WHERE user_id = ? AND project_name = ?',
      args: [userId, projectName]
    });
    const row = getRow(result);
    if (!row) return null;
    return {
      elements: JSON.parse(row.elements || '[]'),
      appState: JSON.parse(row.app_state || '{}'),
      updatedAt: row.updated_at,
    };
  },

  delete: async (userId, projectName) => {
    const result = await db.execute({
      sql: 'DELETE FROM project_canvases WHERE user_id = ? AND project_name = ?',
      args: [userId, projectName]
    });
    return result.rowsAffected > 0;
  },
};

// ─── Password Reset Tokens DB ────────────────────────────────────────────────

const resetTokenDb = {
  create: async (userId, token, expiresAt) => {
    const result = await db.execute({
      sql: 'INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)',
      args: [userId, token, expiresAt]
    });
    return { id: Number(result.lastInsertRowid), token };
  },

  getValid: async (token) => {
    const result = await db.execute({
      sql: 'SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > datetime(\'now\')',
      args: [token]
    });
    return getRow(result);
  },

  markUsed: async (tokenId) => {
    await db.execute({
      sql: 'UPDATE password_reset_tokens SET used = 1 WHERE id = ?',
      args: [tokenId]
    });
  },

  cleanExpired: async () => {
    await db.execute("DELETE FROM password_reset_tokens WHERE expires_at < datetime('now') OR used = 1");
  }
};

// ─── Projects DB (cloud mode) ─────────────────────────────────────────────────

const projectDb = {
  upsert: async (userId, originalPath, displayName = null) => {
    const projectName = originalPath.replace(/[\\/:\s~_]/g, '-');
    const existing = await db.execute({
      sql: 'SELECT id FROM user_projects WHERE user_id = ? AND original_path = ?',
      args: [userId, originalPath]
    });
    if (existing.rows.length > 0) {
      if (displayName) {
        await db.execute({ sql: 'UPDATE user_projects SET display_name = ? WHERE id = ?', args: [displayName, existing.rows[0].id] });
      }
      return { id: Number(existing.rows[0].id), projectName, originalPath, alreadyExists: true };
    }
    const result = await db.execute({
      sql: 'INSERT INTO user_projects (user_id, project_name, original_path, display_name) VALUES (?, ?, ?, ?)',
      args: [userId, projectName, originalPath, displayName]
    });
    return { id: Number(result.lastInsertRowid), projectName, originalPath };
  },

  getAll: async (userId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM user_projects WHERE user_id = ? ORDER BY created_at DESC',
      args: [userId]
    });
    return result.rows;
  },

  remove: async (userId, originalPath) => {
    const result = await db.execute({
      sql: 'DELETE FROM user_projects WHERE user_id = ? AND original_path = ?',
      args: [userId, originalPath]
    });
    return result.rowsAffected > 0;
  },

  rename: async (userId, originalPath, displayName) => {
    const result = await db.execute({
      sql: 'UPDATE user_projects SET display_name = ? WHERE user_id = ? AND original_path = ?',
      args: [displayName, userId, originalPath]
    });
    return result.rowsAffected > 0;
  }
};

// ─── Voice Calls DB ───────────────────────────────────────────────────────────

const voiceCallDb = {
  // Create a call record when call starts (or when we first get a webhook)
  create: async (userId, callType = 'voice', sessionId = null) => {
    const result = await db.execute({
      sql: 'INSERT INTO voice_calls (user_id, call_type, session_id, status) VALUES (?, ?, ?, ?)',
      args: [userId, callType, sessionId, 'in_progress']
    });
    return Number(result.lastInsertRowid);
  },

  // Update call with VAPI call ID (from first webhook event)
  setVapiCallId: async (id, vapiCallId) => {
    await db.execute({
      sql: 'UPDATE voice_calls SET vapi_call_id = ? WHERE id = ?',
      args: [vapiCallId, id]
    });
  },

  // Complete a call with end-of-call-report data
  complete: async (userId, vapiCallId, data) => {
    const { status, endedReason, durationSeconds, cost, transcript, summary, toolsUsed, messagesCount } = data;
    // Try to update existing row by vapi_call_id, or by most recent in-progress call
    const existing = vapiCallId
      ? await db.execute({ sql: 'SELECT id FROM voice_calls WHERE vapi_call_id = ?', args: [vapiCallId] })
      : await db.execute({ sql: 'SELECT id FROM voice_calls WHERE user_id = ? AND status = ? ORDER BY started_at DESC LIMIT 1', args: [userId, 'in_progress'] });

    if (existing.rows.length > 0) {
      await db.execute({
        sql: `UPDATE voice_calls SET status = ?, ended_reason = ?, ended_at = CURRENT_TIMESTAMP,
              duration_seconds = ?, cost = ?, transcript = ?, summary = ?, tools_used = ?,
              messages_count = ? WHERE id = ?`,
        args: [status || 'completed', endedReason || null, durationSeconds || null, cost || null,
               transcript || null, summary || null, toolsUsed || null, messagesCount || 0,
               existing.rows[0].id]
      });
      return Number(existing.rows[0].id);
    }

    // No existing row — insert complete record
    const result = await db.execute({
      sql: `INSERT INTO voice_calls (user_id, vapi_call_id, call_type, status, ended_reason, ended_at,
            duration_seconds, cost, transcript, summary, tools_used, messages_count)
            VALUES (?, ?, 'voice', ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?)`,
      args: [userId, vapiCallId || null, status || 'completed', endedReason || null,
             durationSeconds || null, cost || null, transcript || null, summary || null,
             toolsUsed || null, messagesCount || 0]
    });
    return Number(result.lastInsertRowid);
  },

  // Save a chat call (non-voice, from VAPI Chat API)
  saveChat: async (userId, sessionId, chatId, messagesCount = 1) => {
    const result = await db.execute({
      sql: `INSERT INTO voice_calls (user_id, vapi_call_id, session_id, call_type, status, ended_at, messages_count)
            VALUES (?, ?, ?, 'chat', 'completed', CURRENT_TIMESTAMP, ?)`,
      args: [userId, chatId || null, sessionId || null, messagesCount]
    });
    return Number(result.lastInsertRowid);
  },

  // Get user's call history
  getByUser: async (userId, limit = 20, offset = 0) => {
    const result = await db.execute({
      sql: 'SELECT * FROM voice_calls WHERE user_id = ? ORDER BY started_at DESC LIMIT ? OFFSET ?',
      args: [userId, limit, offset]
    });
    return result.rows;
  },

  // Get user stats (total calls, total duration, etc.)
  getUserStats: async (userId) => {
    const result = await db.execute({
      sql: `SELECT
              COUNT(*) as total_calls,
              SUM(CASE WHEN call_type = 'voice' THEN 1 ELSE 0 END) as voice_calls,
              SUM(CASE WHEN call_type = 'chat' THEN 1 ELSE 0 END) as chat_calls,
              SUM(COALESCE(duration_seconds, 0)) as total_duration_seconds,
              SUM(COALESCE(messages_count, 0)) as total_messages,
              MAX(started_at) as last_call_at
            FROM voice_calls WHERE user_id = ?`,
      args: [userId]
    });
    return getRow(result) || { total_calls: 0, voice_calls: 0, chat_calls: 0, total_duration_seconds: 0, total_messages: 0, last_call_at: null };
  },

  // Get call by VAPI call ID
  getByVapiId: async (vapiCallId) => {
    const result = await db.execute({
      sql: 'SELECT * FROM voice_calls WHERE vapi_call_id = ?',
      args: [vapiCallId]
    });
    return getRow(result);
  },
};

export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, relayTokensDb, githubTokensDb, subscriptionDb, paymentDb, webhookDb, workflowDb, fileVersionDb, sessionUsageDb, connectionDb, canvasDb, resetTokenDb, projectDb, voiceCallDb, PLAN_DURATIONS };
