"use strict";

const { load, currentTarget } = require("@neon-rs/load");
const { familySync, GLIBC, MUSL } = require("detect-libc");

function requireNative() {
  if (process.env.LIBSQL_JS_DEV) {
    return load(__dirname)
  }
  let target = currentTarget();
  // Workaround for Bun, which reports a musl target, but really wants glibc...
  if (familySync() == GLIBC) {
    switch (target) {
      case "linux-x64-musl":
        target = "linux-x64-gnu";
        break;
      case "linux-arm64-musl":
        target = "linux-arm64-gnu";
        break;
    }
  }
  // @neon-rs/load doesn't detect arm musl
  if (target === "linux-arm-gnueabihf" && familySync() == MUSL) {
      target = "linux-arm-musleabihf";
  }
  return require(`@libsql/${target}`);
}

const {
  databaseOpen,
  databaseOpenWithSync,
  databaseInTransaction,
  databaseInterrupt,
  databaseClose,
  databaseSyncSync,
  databaseSyncUntilSync,
  databaseExecSync,
  databasePrepareSync,
  databaseDefaultSafeIntegers,
  databaseAuthorizer,
  databaseLoadExtension,
  databaseMaxWriteReplicationIndex,
  statementRaw,
  statementIsReader,
  statementGet,
  statementRun,
  statementInterrupt,
  statementRowsSync,
  statementColumns,
  statementSafeIntegers,
  rowsNext,
} = requireNative();

const Authorization = require("./auth");
const SqliteError = require("./sqlite-error");

function convertError(err) {
  if (err.libsqlError) {
    return new SqliteError(err.message, err.code, err.rawCode);
  }
  return err;
}

/**
 * Database represents a connection that can prepare and execute SQL statements.
 */
class Database {
  /**
   * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created.
   *
   * @constructor
   * @param {string} path - Path to the database file.
   */
  constructor(path, opts) {
    const encryptionCipher = opts?.encryptionCipher ?? "aes256cbc";
    if (opts && opts.syncUrl) {
      var authToken = "";
      if (opts.syncAuth) {
          console.warn("Warning: The `syncAuth` option is deprecated, please use `authToken` option instead.");
          authToken = opts.syncAuth;
      } else if (opts.authToken) {
          authToken = opts.authToken;
      }
      const encryptionKey = opts?.encryptionKey ?? "";
      const syncPeriod = opts?.syncPeriod ?? 0.0;
      const readYourWrites = opts?.readYourWrites ?? true;
      const offline = opts?.offline ?? false;
      const remoteEncryptionKey = opts?.remoteEncryptionKey ?? "";
      this.db = databaseOpenWithSync(path, opts.syncUrl, authToken, encryptionCipher, encryptionKey, syncPeriod, readYourWrites, offline, remoteEncryptionKey);
    } else {
      const authToken = opts?.authToken ?? "";
      const encryptionKey = opts?.encryptionKey ?? "";
      const timeout = opts?.timeout ?? 0.0;
      const remoteEncryptionKey = opts?.remoteEncryptionKey ?? "";
      this.db = databaseOpen(path, authToken, encryptionCipher, encryptionKey, timeout, remoteEncryptionKey);
    }
    // TODO: Use a libSQL API for this?
    this.memory = path === ":memory:";
    this.readonly = false;
    this.name = "";
    this.open = true;

    const db = this.db;
    Object.defineProperties(this, {
      inTransaction: {
        get() {
          return databaseInTransaction(db);
        }
      },
    });
  }

  sync() {
    return databaseSyncSync.call(this.db);
  }

  syncUntil(replicationIndex) {
    return databaseSyncUntilSync.call(this.db, replicationIndex);
  }

  /**
   * Prepares a SQL statement for execution.
   *
   * @param {string} sql - The SQL statement string to prepare.
   */
  prepare(sql) {
    try {
      const stmt = databasePrepareSync.call(this.db, sql);
      return new Statement(stmt);
    } catch (err) {
      throw convertError(err);
    }
  }

  /**
   * Returns a function that executes the given function in a transaction.
   *
   * @param {function} fn - The function to wrap in a transaction.
   */
  transaction(fn) {
    if (typeof fn !== "function")
      throw new TypeError("Expected first argument to be a function");

    const db = this;
    const wrapTxn = (mode) => {
      return (...bindParameters) => {
        db.exec("BEGIN " + mode);
        try {
          const result = fn(...bindParameters);
          db.exec("COMMIT");
          return result;
        } catch (err) {
          db.exec("ROLLBACK");
          throw err;
        }
      };
    };
    const properties = {
      default: { value: wrapTxn("") },
      deferred: { value: wrapTxn("DEFERRED") },
      immediate: { value: wrapTxn("IMMEDIATE") },
      exclusive: { value: wrapTxn("EXCLUSIVE") },
      database: { value: this, enumerable: true },
    };
    Object.defineProperties(properties.default.value, properties);
    Object.defineProperties(properties.deferred.value, properties);
    Object.defineProperties(properties.immediate.value, properties);
    Object.defineProperties(properties.exclusive.value, properties);
    return properties.default.value;
  }

  pragma(source, options) {
    if (options == null) options = {};
    if (typeof source !== 'string') throw new TypeError('Expected first argument to be a string');
    if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object');
    const simple = options['simple'];
    const stmt = this.prepare(`PRAGMA ${source}`, this, true);
    return simple ? stmt.pluck().get() : stmt.all();
  }

  backup(filename, options) {
    throw new Error("not implemented");
  }

  serialize(options) {
    throw new Error("not implemented");
  }

  function(name, options, fn) {
    // Apply defaults
    if (options == null) options = {};
    if (typeof options === "function") {
      fn = options;
      options = {};
    }

    // Validate arguments
    if (typeof name !== "string")
      throw new TypeError("Expected first argument to be a string");
    if (typeof fn !== "function")
      throw new TypeError("Expected last argument to be a function");
    if (typeof options !== "object")
      throw new TypeError("Expected second argument to be an options object");
    if (!name)
      throw new TypeError(
        "User-defined function name cannot be an empty string"
      );

    throw new Error("not implemented");
  }

  aggregate(name, options) {
    // Validate arguments
    if (typeof name !== "string")
      throw new TypeError("Expected first argument to be a string");
    if (typeof options !== "object" || options === null)
      throw new TypeError("Expected second argument to be an options object");
    if (!name)
      throw new TypeError(
        "User-defined function name cannot be an empty string"
      );

    throw new Error("not implemented");
  }

  table(name, factory) {
    // Validate arguments
    if (typeof name !== "string")
      throw new TypeError("Expected first argument to be a string");
    if (!name)
      throw new TypeError(
        "Virtual table module name cannot be an empty string"
      );

    throw new Error("not implemented");
  }

  authorizer(rules) {
    databaseAuthorizer.call(this.db, rules);
  }

  loadExtension(...args) {
    databaseLoadExtension.call(this.db, ...args);
  }

  maxWriteReplicationIndex() {
    return databaseMaxWriteReplicationIndex.call(this.db)
  }

  /**
   * Executes a SQL statement.
   *
   * @param {string} sql - The SQL statement string to execute.
   */
  exec(sql) {
    try {
      databaseExecSync.call(this.db, sql);
    } catch (err) {
      throw convertError(err);
    }
  }

  /**
   * Interrupts the database connection.
   */
  interrupt() {
    databaseInterrupt.call(this.db);
  }

  /**
   * Closes the database connection.
   */
  close() {
    databaseClose.call(this.db);
    this.open = false;
  }

  /**
   * Toggle 64-bit integer support.
   */
  defaultSafeIntegers(toggle) {
    databaseDefaultSafeIntegers.call(this.db, toggle ?? true);
    return this;
  }

  unsafeMode(...args) {
    throw new Error("not implemented");
  }
}

/**
 * Statement represents a prepared SQL statement that can be executed.
 */
class Statement {
  constructor(stmt) {
    this.stmt = stmt;
    this.pluckMode = false;
  }

  /**
   * Toggle raw mode.
   *
   * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled.
   */
  raw(raw) {
    statementRaw.call(this.stmt, raw ?? true);
    return this;
  }

  /**
   * Toggle pluck mode.
   *
   * @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled.
   */
  pluck(pluckMode) {
    this.pluckMode = pluckMode ?? true;
    return this;
  }

  get reader() {
    return statementIsReader.call(this.stmt);
  }

  /**
   * Executes the SQL statement and returns an info object.
   */
  run(...bindParameters) {
    try {
      if (bindParameters.length == 1 && typeof bindParameters[0] === "object") {
        return statementRun.call(this.stmt, bindParameters[0]);
      } else {
        return statementRun.call(this.stmt, bindParameters.flat());
      }
    } catch (err) {
      throw convertError(err);
    }
  }

  /**
   * Executes the SQL statement and returns the first row.
   *
   * @param bindParameters - The bind parameters for executing the statement.
   */
  get(...bindParameters) {
    try {
      if (bindParameters.length == 1 && typeof bindParameters[0] === "object") {
        return statementGet.call(this.stmt, bindParameters[0]);
      } else {
        return statementGet.call(this.stmt, bindParameters.flat());
      }
    } catch (err) {
      throw convertError(err);
    }
  }

  /**
   * Executes the SQL statement and returns an iterator to the resulting rows.
   *
   * @param bindParameters - The bind parameters for executing the statement.
   */
  iterate(...bindParameters) {
    var rows = undefined;
    if (bindParameters.length == 1 && typeof bindParameters[0] === "object") {
      rows = statementRowsSync.call(this.stmt, bindParameters[0]);
    } else {
      rows = statementRowsSync.call(this.stmt, bindParameters.flat());
    }
    const iter = {
      nextRows: Array(100),
      nextRowIndex: 100,
      next() {
        try {
          if (this.nextRowIndex === 100) {
            rowsNext.call(rows, this.nextRows);
            this.nextRowIndex = 0;
          }
          const row = this.nextRows[this.nextRowIndex];
          this.nextRows[this.nextRowIndex] = undefined;
          if (!row) {
            return { done: true };
          }
        this.nextRowIndex++;
          return { value: row, done: false };
        } catch (err) {
          throw convertError(err);
        }
      },
      [Symbol.iterator]() {
        return this;
      },
    };
    return iter;
  }

  /**
   * Executes the SQL statement and returns an array of the resulting rows.
   *
   * @param bindParameters - The bind parameters for executing the statement.
   */
  all(...bindParameters) {
    try {
      const result = [];
      for (const row of this.iterate(...bindParameters)) {
        if (this.pluckMode) {
          result.push(row[Object.keys(row)[0]]);
        } else {
          result.push(row);
        }
      }
      return result;
    } catch (err) {
      throw convertError(err);
    }
  }

  /**
   * Interrupts the statement.
   */
  interrupt() {
    statementInterrupt.call(this.stmt);
  }

  /**
   * Returns the columns in the result set returned by this prepared statement.
   */
  columns() {
    return statementColumns.call(this.stmt);
  }

  /**
   * Toggle 64-bit integer support.
   */
  safeIntegers(toggle) {
    statementSafeIntegers.call(this.stmt, toggle ?? true);
    return this;
  }
}

module.exports = Database;
module.exports.Authorization = Authorization;
module.exports.SqliteError = SqliteError;
