package tech.turso.core;

import java.sql.SQLException;
import tech.turso.annotations.NativeInvocation;
import tech.turso.annotations.Nullable;
import tech.turso.utils.Logger;
import tech.turso.utils.LoggerFactory;
import tech.turso.utils.TursoExceptionUtils;

/**
 * By default, only one <code>resultSet</code> object per <code>TursoStatement</code> can be open at
 * the same time. Therefore, if the reading of one <code>resultSet</code> object is interleaved with
 * the reading of another, each must have been generated by different <code>TursoStatement</code>
 * objects. All execution method in the <code>TursoStatement</code> implicitly close the current
 * <code>resultSet</code> object of the statement if an open one exists.
 */
public final class TursoStatement {

  private static final Logger log = LoggerFactory.getLogger(TursoStatement.class);

  private final String sql;
  private final long statementPointer;
  private final TursoResultSet resultSet;

  private boolean closed;

  // TODO: what if the statement we ran was DDL, update queries and etc. Should we still create a
  // resultSet?
  public TursoStatement(String sql, long statementPointer) {
    this.sql = sql;
    this.statementPointer = statementPointer;
    this.resultSet = TursoResultSet.of(this);
    log.debug("Creating statement with sql: {}", this.sql);
  }

  public TursoResultSet getResultSet() {
    return resultSet;
  }

  /**
   * Expects a clean statement created right after prepare method is called.
   *
   * @return true if the ResultSet has at least one row; false otherwise.
   */
  public boolean execute() throws SQLException {
    resultSet.next();
    return resultSet.hasLastStepReturnedRow();
  }

  TursoStepResult step() throws SQLException {
    final TursoStepResult result = step(this.statementPointer);
    if (result == null) {
      throw new SQLException("step() returned null, which is only returned when an error occurs");
    }

    return result;
  }

  /**
   * Because turso supports async I/O, it is possible to return a {@link TursoStepResult} with
   * {@link TursoStepResult#STEP_RESULT_ID_ROW}. However, this is handled by the native side, so you
   * can expect that this method will not return a {@link TursoStepResult#STEP_RESULT_ID_ROW}.
   */
  @Nullable
  private native TursoStepResult step(long stmtPointer) throws SQLException;

  /**
   * Throws formatted SQLException with error code and message.
   *
   * @param errorCode Error code.
   * @param errorMessageBytes Error message.
   */
  @NativeInvocation(invokedFrom = "turso_statement.rs")
  private void throwTursoException(int errorCode, byte[] errorMessageBytes) throws SQLException {
    TursoExceptionUtils.throwTursoException(errorCode, errorMessageBytes);
  }

  /**
   * Closes the current statement and releases any resources associated with it. This method calls
   * the native `_close` method to perform the actual closing operation.
   */
  public void close() throws SQLException {
    if (closed) {
      return;
    }
    this.resultSet.close();
    _close(statementPointer);
    closed = true;
  }

  private native void _close(long statementPointer);

  /**
   * Initializes the column metadata, such as the names of the columns. Since {@link tursoStatement}
   * can only have a single {@link tursoResultSet}, it is appropriate to place the initialization of
   * column metadata here.
   *
   * @throws SQLException if a database access error occurs while retrieving column names
   */
  public void initializeColumnMetadata() throws SQLException {
    final String[] columnNames = this.columns(statementPointer);
    if (columnNames != null) {
      this.resultSet.setColumnNames(columnNames);
    }
  }

  @Nullable
  private native String[] columns(long statementPointer) throws SQLException;

  /**
   * Binds a NULL value to the prepared statement at the specified position.
   *
   * @param position The index of the SQL parameter to be set to NULL.
   * @return <a href="https://www.sqlite.org/c3ref/c_abort.html">Result Codes</a>
   * @throws SQLException If a database access error occurs.
   */
  public int bindNull(int position) throws SQLException {
    final int result = bindNull(statementPointer, position);
    if (result != 0) {
      throw new SQLException("Exception while binding NULL value at position " + position);
    }
    return result;
  }

  private native int bindNull(long statementPointer, int position) throws SQLException;

  /**
   * Binds an integer value to the prepared statement at the specified position. This function calls
   * bindLong because turso treats all integers as long (as well as SQLite).
   *
   * <p>According to SQLite documentation, the value is a signed integer, stored in 0, 1, 2, 3, 4,
   * 6, or 8 bytes depending on the magnitude of the value.
   *
   * @param position The index of the SQL parameter to be set.
   * @param value The integer value to bind to the parameter.
   * @return A result code indicating the success or failure of the operation.
   * @throws SQLException If a database access error occurs.
   */
  public int bindInt(int position, int value) throws SQLException {
    return bindLong(position, value);
  }

  /**
   * Binds a long value to the prepared statement at the specified position.
   *
   * @param position The index of the SQL parameter to be set.
   * @param value The value to bind to the parameter.
   * @return <a href="https://www.sqlite.org/c3ref/c_abort.html">Result Codes</a>
   * @throws SQLException If a database access error occurs.
   */
  public int bindLong(int position, long value) throws SQLException {
    final int result = bindLong(statementPointer, position, value);
    if (result != 0) {
      throw new SQLException("Exception while binding long value at position " + position);
    }
    return result;
  }

  private native int bindLong(long statementPointer, int position, long value) throws SQLException;

  /**
   * Binds a double value to the prepared statement at the specified position.
   *
   * @param position The index of the SQL parameter to be set.
   * @param value The value to bind to the parameter.
   * @return <a href="https://www.sqlite.org/c3ref/c_abort.html">Result Codes</a>
   * @throws SQLException If a database access error occurs.
   */
  public int bindDouble(int position, double value) throws SQLException {
    final int result = bindDouble(statementPointer, position, value);
    if (result != 0) {
      throw new SQLException("Exception while binding double value at position " + position);
    }
    return result;
  }

  private native int bindDouble(long statementPointer, int position, double value)
      throws SQLException;

  /**
   * Binds a text value to the prepared statement at the specified position.
   *
   * @param position The index of the SQL parameter to be set.
   * @param value The value to bind to the parameter.
   * @return <a href="https://www.sqlite.org/c3ref/c_abort.html">Result Codes</a>
   * @throws SQLException If a database access error occurs.
   */
  public int bindText(int position, String value) throws SQLException {
    final int result = bindText(statementPointer, position, value);
    if (result != 0) {
      throw new SQLException("Exception while binding text value at position " + position);
    }
    return result;
  }

  private native int bindText(long statementPointer, int position, String value)
      throws SQLException;

  /**
   * Binds a blob value to the prepared statement at the specified position.
   *
   * @param position The index of the SQL parameter to be set.
   * @param value The value to bind to the parameter.
   * @return <a href="https://www.sqlite.org/c3ref/c_abort.html">Result Codes</a>
   * @throws SQLException If a database access error occurs.
   */
  public int bindBlob(int position, byte[] value) throws SQLException {
    final int result = bindBlob(statementPointer, position, value);
    if (result != 0) {
      throw new SQLException("Exception while binding blob value at position " + position);
    }
    return result;
  }

  private native int bindBlob(long statementPointer, int position, byte[] value)
      throws SQLException;

  /**
   * Returns total number of changes.
   *
   * @throws SQLException If a database access error occurs
   */
  public long totalChanges() throws SQLException {
    final long result = totalChanges(statementPointer);
    if (result == -1) {
      throw new SQLException("Exception while retrieving total number of changes");
    }

    return result;
  }

  private native long totalChanges(long statementPointer) throws SQLException;

  /**
   * Returns number of changes.
   *
   * @throws SQLException If a database access error occurs
   */
  public long changes() throws SQLException {
    final long result = changes(statementPointer);
    if (result == -1) {
      throw new SQLException("Exception while retrieving number of changes");
    }

    return result;
  }

  private native long changes(long statementPointer) throws SQLException;

  /**
   * Checks if the statement is closed.
   *
   * @return true if the statement is closed, false otherwise.
   */
  public boolean isClosed() {
    return closed;
  }

  @Override
  public String toString() {
    return ("tursoStatement{"
        + "statementPointer="
        + statementPointer
        + ", sql='"
        + sql
        + '\''
        + '}');
  }
}
