"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Database = void 0;
var _AsyncLock = require("./AsyncLock");
var _Statement = require("./Statement");
var _asyncOperation = require("./internal/asyncOperation");
var _ioProcessor = require("./internal/ioProcessor");
/**
 * Database
 *
 * Unified high-level API for both local and sync databases.
 * Constructor determines whether to use local-only or sync mode based on config.
 */

/**
 * Check if config has sync properties (url field)
 */
function isSyncConfig(opts) {
  return opts.url !== undefined && opts.url !== null;
}

/**
 * Calculate reserved bytes based on encryption cipher.
 * These values match the Turso Cloud encryption settings.
 */
function getReservedBytesForCipher(encryption) {
  if (!encryption) {
    return 0;
  }
  switch (encryption.cipher) {
    case 'aes256gcm':
    case 'aes128gcm':
    case 'chacha20poly1305':
      return 28;
    case 'aegis128l':
    case 'aegis128x2':
    case 'aegis128x4':
      return 32;
    case 'aegis256':
    case 'aegis256x2':
    case 'aegis256x4':
      return 48;
    default:
      return 0;
  }
}

/**
 * Database class - works for both local-only and sync databases
 *
 * All database operations are async to properly handle IO requirements:
 * - For local databases: async allows yielding to JS event loop
 * - For sync databases: async required for network operations
 * - For partial sync: async required to load missing pages on-demand
 */
class Database {
  _nativeDb = null;
  _nativeSyncDb = null;
  _connection = null;
  _isSync = false;
  _connected = false;
  _closed = false;
  /**
   * Create a new database (doesn't connect yet - call connect())
   *
   * @param opts - Database options
   */
  constructor(opts) {
    this._opts = opts;
    this._isSync = isSyncConfig(opts);
    this._execLock = new _AsyncLock.AsyncLock();
  }

  /**
   * Connect to the database (matches JavaScript bindings)
   * For local databases: opens immediately
   * For sync databases: bootstraps if needed
   */
  async connect() {
    if (this._connected) {
      return;
    }
    if (this._isSync) {
      await this.initSyncDatabase();
    } else {
      this.initLocalDatabase();
    }
    this._connected = true;
  }

  /**
   * Initialize local-only database
   */
  initLocalDatabase() {
    if (typeof __TursoProxy === 'undefined') {
      throw new Error('Turso native module not loaded');
    }
    const dbConfig = {
      path: this._opts.path,
      async_io: false // use blocking IO for local database
    };

    // Create native database (path normalization happens in C++ JSI layer)
    this._nativeDb = __TursoProxy.newDatabase(this._opts.path, dbConfig);

    // Open database
    this._nativeDb.open();

    // Get connection
    this._connection = this._nativeDb.connect();
  }

  /**
   * Initialize sync database
   */
  async initSyncDatabase() {
    if (typeof __TursoProxy === 'undefined') {
      throw new Error('Turso native module not loaded');
    }

    // Get URL (can be string or function)
    let url = null;
    if (typeof this._opts.url === 'function') {
      url = this._opts.url();
    } else if (typeof this._opts.url === 'string') {
      url = this._opts.url;
    }

    // Build dbConfig (path normalization happens in C++ JSI layer)
    const dbConfig = {
      path: this._opts.path,
      async_io: true // use async IO for synced database as we have network IO loop externally from the turso core
    };

    // Calculate reserved bytes from cipher
    const reservedBytes = getReservedBytesForCipher(this._opts.remoteEncryption);

    // Build syncConfig with all options
    const syncConfig = {
      remoteUrl: url,
      clientName: this._opts.clientName || 'turso-sync-react-native',
      longPollTimeoutMs: this._opts.longPollTimeoutMs,
      bootstrapIfEmpty: this._opts.bootstrapIfEmpty ?? true,
      reservedBytes: reservedBytes,
      // Remote encryption options (key is passed to sync engine for HTTP headers)
      remoteEncryptionKey: this._opts.remoteEncryption?.key,
      remoteEncryptionCipher: this._opts.remoteEncryption?.cipher
    };

    // Add partial sync options if present
    if (this._opts.partialSyncExperimental) {
      const partial = this._opts.partialSyncExperimental;
      if (partial.bootstrapStrategy.kind === 'prefix') {
        syncConfig.partialBootstrapStrategyPrefix = partial.bootstrapStrategy.length;
      } else if (partial.bootstrapStrategy.kind === 'query') {
        syncConfig.partialBootstrapStrategyQuery = partial.bootstrapStrategy.query;
      }
      syncConfig.partialBootstrapSegmentSize = partial.segmentSize;
      syncConfig.partialBootstrapPrefetch = partial.prefetch;
    }

    // Create native sync database
    this._nativeSyncDb = __TursoProxy.newSyncDatabase(dbConfig, syncConfig);

    // Create IO context with auth token and base URL
    this._ioContext = {
      authToken: this._opts.authToken,
      baseUrl: this._opts.url
    };

    // Create extraIo callback for partial sync support
    // This callback drains the sync engine's IO queue during statement execution
    this._extraIo = async () => {
      if (this._nativeSyncDb && this._ioContext) {
        await (0, _ioProcessor.drainSyncIo)(this._nativeSyncDb, this._ioContext);
      }
    };

    // Bootstrap/open database
    const operation = this._nativeSyncDb.create();
    await (0, _asyncOperation.driveVoidOperation)(operation, this._nativeSyncDb, this._ioContext);

    // Get connection
    const connOperation = this._nativeSyncDb.connect();
    this._connection = await (0, _asyncOperation.driveConnectionOperation)(connOperation, this._nativeSyncDb, this._ioContext);
  }

  /**
   * Prepare a SQL statement
   *
   * @param sql - SQL statement to prepare
   * @returns Prepared statement
   */
  prepare(sql) {
    this.checkOpen();
    if (!this._connection) {
      throw new Error('No connection available');
    }
    const nativeStmt = this._connection.prepareSingle(sql);
    return new _Statement.Statement(nativeStmt, this._connection, this._execLock, this._extraIo);
  }

  /**
   * Execute SQL without returning results (for DDL, multi-statement SQL)
   *
   * @param sql - SQL to execute
   */
  async exec(sql) {
    this.checkOpen();
    if (!this._connection) {
      throw new Error('No connection available');
    }
    await this._execLock.acquire();
    try {
      // Use prepareFirst to handle multiple statements
      let remaining = sql.trim();
      while (remaining.length > 0) {
        const result = this._connection.prepareFirst(remaining);
        if (!result) {
          break; // No more statements (C++ returns null when nothing to parse)
        }

        // Wrap in Statement to get IO handling (no lock — we already hold it)
        const stmt = new _Statement.Statement(result.statement, this._connection, null, this._extraIo);
        try {
          // Execute - will handle IO if needed
          await stmt.rawRun();
        } finally {
          stmt.finalize();
        }

        // Move to next statement
        remaining = sql.substring(result.tailIdx).trim();
      }
    } finally {
      this._execLock.release();
    }
  }

  /**
   * Execute statement and return result info
   *
   * @param sql - SQL statement
   * @param params - Bind parameters
   * @returns Run result with changes and lastInsertRowid
   */
  async run(sql, ...params) {
    const stmt = this.prepare(sql);
    try {
      return await stmt.run(...params);
    } finally {
      stmt.finalize();
    }
  }

  /**
   * Execute query and return first row
   *
   * @param sql - SQL query
   * @param params - Bind parameters
   * @returns First row or undefined
   */
  async get(sql, ...params) {
    const stmt = this.prepare(sql);
    try {
      return await stmt.get(...params);
    } finally {
      stmt.finalize();
    }
  }

  /**
   * Execute query and return all rows
   *
   * @param sql - SQL query
   * @param params - Bind parameters
   * @returns All rows
   */
  async all(sql, ...params) {
    const stmt = this.prepare(sql);
    try {
      return await stmt.all(...params);
    } finally {
      stmt.finalize();
    }
  }

  /**
   * Execute function within a transaction
   *
   * @param fn - Function to execute
   * @returns Function result
   */
  async transaction(fn) {
    this.checkOpen();
    await this.exec('BEGIN');
    try {
      const result = await fn();
      await this.exec('COMMIT');
      return result;
    } catch (error) {
      await this.exec('ROLLBACK');
      throw error;
    }
  }

  /**
   * Push local changes to remote (sync databases only)
   */
  async push() {
    if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
      throw new Error('push() is only available for sync databases');
    }
    const operation = this._nativeSyncDb.pushChanges();
    await (0, _asyncOperation.driveVoidOperation)(operation, this._nativeSyncDb, this._ioContext);
  }

  /**
   * Pull remote changes and apply locally (sync databases only)
   *
   * @returns true if changes were applied, false if no changes
   */
  async pull() {
    if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
      throw new Error('pull() is only available for sync databases');
    }

    // Wait for changes
    const waitOperation = this._nativeSyncDb.waitChanges();
    const changes = await (0, _asyncOperation.driveChangesOperation)(waitOperation, this._nativeSyncDb, this._ioContext);

    // If no changes, return false
    if (!changes) {
      return false;
    }

    // Apply changes
    const applyOperation = this._nativeSyncDb.applyChanges(changes);
    await (0, _asyncOperation.driveVoidOperation)(applyOperation, this._nativeSyncDb, this._ioContext);
    return true;
  }

  /**
   * Get sync statistics (sync databases only)
   *
   * @returns Sync stats
   */
  async stats() {
    if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
      throw new Error('stats() is only available for sync databases');
    }
    const operation = this._nativeSyncDb.stats();
    return (0, _asyncOperation.driveStatsOperation)(operation, this._nativeSyncDb, this._ioContext);
  }

  /**
   * Checkpoint database (sync databases only)
   */
  async checkpoint() {
    if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
      throw new Error('checkpoint() is only available for sync databases');
    }
    const operation = this._nativeSyncDb.checkpoint();
    await (0, _asyncOperation.driveVoidOperation)(operation, this._nativeSyncDb, this._ioContext);
  }

  /**
   * Close the database
   */
  close() {
    if (this._closed) {
      return;
    }
    if (this._connection) {
      this._connection.close();
      this._connection = null;
    }
    if (this._nativeDb) {
      this._nativeDb.close();
      this._nativeDb = null;
    }
    if (this._nativeSyncDb) {
      this._nativeSyncDb.close();
      this._nativeSyncDb = null;
    }
    this._connected = false;
    this._closed = true;
  }

  /**
   * Get database path
   */
  get path() {
    return this._opts.path;
  }

  /**
   * Check if database is a sync database
   */
  get isSync() {
    return this._isSync;
  }

  /**
   * Check if database is open
   */
  get open() {
    return !this._closed && this._connection !== null;
  }

  /**
   * Check if in transaction
   */
  get inTransaction() {
    if (!this._connection) {
      return false;
    }
    return !this._connection.getAutocommit();
  }

  /**
   * Get last insert rowid
   */
  get lastInsertRowid() {
    if (!this._connection) {
      return 0;
    }
    return this._connection.lastInsertRowid();
  }

  /**
   * Check if open and throw if not
   */
  checkOpen() {
    if (this._closed) {
      throw new Error('Database is closed');
    }
    if (!this._connected || !this._connection) {
      throw new Error('Database not connected. Call connect() first.');
    }
  }
}
exports.Database = Database;
//# sourceMappingURL=Database.js.map