package ru.micord.ervu_eks.component.service;

import java.sql.Array;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import bpmn.handler.sql_handler.SQLHandlerUtils;
import bpmn.handler.sql_handler.SQLParameters;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DefaultDataType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import service.button.sql.ExecuteSqlButtonService;
import utils.SqlQueryUtils;

import ru.cg.webbpm.modules.core.runtime.api.context.ExecutionContextHelper;
import ru.cg.webbpm.modules.database.api.provider.DslProvider;
import ru.cg.webbpm.modules.standard_annotations.editor.TextAreaEditor;
import ru.cg.webbpm.modules.standard_annotations.validation.NotNull;

public class ReadOnlySqlButtonServiceImpl implements ExecuteSqlButtonService {
  private static final Logger logger = LoggerFactory.getLogger(ReadOnlySqlButtonServiceImpl.class);
  @Autowired
  private DslProvider dslProvider;
  @Autowired
  protected ExecutionContextHelper executionContextHelper;
  private DSLContext dsl;
  @TextAreaEditor
  @NotNull
  public String sql;
  public String jndiName;

  public void initDsl() {

    if (dsl != null) {
      return;
    }
    dsl = this.dslProvider.getDslContext(jndiName);
    if (dsl == null) {
      throw new RuntimeException("Couldn't get dslContext with datasourceJndiName = " + jndiName);
    }
  }

  public List<?> executeSql(Object[] params) {
    initDsl();
    SQLParameters parameters = new SQLParameters();
    parameters.setSql(sql);
    parameters.setSqlParameters(params);

    return switch (SqlQueryUtils.queryType(sql)) {
      case SELECT -> executeSelect(parameters);
      case UNKNOWN -> executeUnknownQuery(parameters);
      default -> Collections.EMPTY_LIST;
    };
  }

  private List<?> executeSelect(SQLParameters parameters) {
    Result<Record> result = executeSelectRaw(parameters);

    if (result.size() > 1) {
      String message = String.format("Query \"%s\" with arguments %s returned more than one row.",
          parameters.getSql(), Arrays.toString(parameters.getSqlParameters())
      );
      throw new IllegalStateException(message);
    }
    Record row = result.get(0);
    List<Object> results = new ArrayList<>();

    for (int i = 0;
         i < row.size();
         i++) {
      results.add(row.getValue(i));
    }
    return results;
  }

  private List<?> executeUnknownQuery(SQLParameters parameters) {
    // jooq does not provide any way to get generated keys for sql queries
    Connection connection = dsl.configuration().connectionProvider().acquire();
    try (PreparedStatement statement = connection.prepareStatement(parameters.getSql())) {
      setParameters(statement, parameters, connection);

      String query = dsl.renderInlined(
          dsl.query(parameters.getSql(), parameters.getSqlParameters()));
      String executionContext =
          "\n with Execution Context: \n" + executionContextHelper.getExecutionContext();
      logger.debug("Executing query " + query + executionContext);

      statement.execute();
      ResultSet resultSet = statement.getResultSet();

      if (resultSet != null) {
        List<Object> results = new ArrayList<>();
        ResultSetMetaData metaData = resultSet.getMetaData();
        while (resultSet.next()) {
          Object[] row = new Object[metaData.getColumnCount()];
          for (int i = 0;
               i < metaData.getColumnCount();
               i++) {
            row[i] = resultSet.getObject(i + 1);
          }
          results.add(row);
        }
        return results;
      }
      else {
        return Collections.emptyList();
      }
    }
    catch (SQLException e) {
      StringBuilder sb = new StringBuilder("Failed to execute SQL query. SQL: \"").append(
          parameters.getSql()).append("\"");
      if (parameters.getSqlParameters() != null && parameters.getSqlParameters().length > 0) {
        String parametersStr = Arrays.toString(parameters.getSqlParameters());
        sb.append(", with parameters: ").append(parametersStr);
      }
      throw new DataAccessException(sb.toString(), e);
    }
    finally {
      dsl.configuration().connectionProvider().release(connection);
    }
  }

  private void setParameters(PreparedStatement statement, SQLParameters parameters,
                             Connection connection) throws SQLException {
    for (int i = 0;
         i < parameters.getSqlParameters().length;
         i++) {
      Object parameter = parameters.getSqlParameters()[i];
      if (parameter == null) {
        // Also documentation states that it's better to use setNull method
        // because not all databases support setObject with null value but
        // in practice we don't know parameter type and the only sql type left
        // for setNull method argument is java.sql.Types.NULL, which is not
        // supported by all databases as well.
        statement.setObject(i + 1, null);
      }
      else {
        if (SQLHandlerUtils.isArrayExceptBytesArray(parameter)) {
          Object[] castParameter = (Object[]) parameter;
          org.jooq.SQLDialect dialect = dsl.configuration().dialect();
          String typeName = DefaultDataType.getDataType(dialect,
              castParameter.getClass().getComponentType()
          ).getTypeName();
          Array arrayOf = connection.createArrayOf(typeName, castParameter);
          statement.setArray(i + 1, arrayOf);
        }
        else {
          statement.setObject(i + 1, parameter);
        }
      }
    }
  }

  private Result<Record> executeSelectRaw(SQLParameters parameters) {
    return dsl.resultQuery(parameters.getSql(), parameters.getSqlParameters()).fetch();
  }
}
