package cn.fateverse.query.submeter.preparestatement;

import cn.fateverse.query.submeter.connection.SubmeterConnection;
import cn.fateverse.query.submeter.executor.ForceExecuteTemplate;
import cn.fateverse.query.submeter.result.SubmeterResult;
import org.apache.shardingsphere.sharding.execute.sql.execute.result.StreamQueryResult;
import org.apache.shardingsphere.sharding.merge.dql.ShardingDQLResultMerger;
import org.apache.shardingsphere.sql.parser.SQLParserEngine;
import org.apache.shardingsphere.sql.parser.SQLParserEngineFactory;
import org.apache.shardingsphere.sql.parser.binder.SQLStatementContextFactory;
import org.apache.shardingsphere.sql.parser.binder.metadata.schema.SchemaMetaData;
import org.apache.shardingsphere.sql.parser.binder.metadata.table.TableMetaData;
import org.apache.shardingsphere.sql.parser.binder.metadata.table.TableMetaDataLoader;
import org.apache.shardingsphere.sql.parser.binder.statement.SQLStatementContext;
import org.apache.shardingsphere.sql.parser.sql.statement.SQLStatement;
import org.apache.shardingsphere.underlying.common.database.type.dialect.MySQLDatabaseType;
import org.apache.shardingsphere.underlying.executor.QueryResult;
import org.apache.shardingsphere.underlying.merge.result.MergedResult;

import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SubmeterPreparedStatement extends AbstractSubmeterPreparedStatementAdapter {

    private final SubmeterConnection connection;

    private final String sql;
    private final int resultSetType;
    private final int resultSetConcurrency;
    private final int resultSetHoldability;
    private final boolean returnGeneratedKeys;

    private final SQLParserEngine parserEngine;

    private ResultSet currentResultSet;


    private final ForceExecuteTemplate<Statement> forceExecuteTemplate = new ForceExecuteTemplate<>();


    public SubmeterPreparedStatement(final SubmeterConnection connection) {
        this(connection, "", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT, false);
    }

    public SubmeterPreparedStatement(final SubmeterConnection connection, final String sql) {
        this(connection, sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT, false);
    }

    public SubmeterPreparedStatement(final SubmeterConnection connection, final String sql, final int resultSetType, final int resultSetConcurrency) {
        this(connection, sql, resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT, false);
    }


    public SubmeterPreparedStatement(final SubmeterConnection connection, final String sql, final int autoGeneratedKeys) {
        this(connection, sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT, Statement.RETURN_GENERATED_KEYS == autoGeneratedKeys);
    }

    public SubmeterPreparedStatement(final SubmeterConnection connection, final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) {
        this(connection, sql, resultSetType, resultSetConcurrency, resultSetHoldability, false);
    }

    public SubmeterPreparedStatement(final SubmeterConnection connection, final int resultSetType, final int resultSetConcurrency) {
        this(connection, "", resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT, false);
    }


    public SubmeterPreparedStatement(final SubmeterConnection connection, final int autoGeneratedKeys) {
        this(connection, "", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT, Statement.RETURN_GENERATED_KEYS == autoGeneratedKeys);
    }

    public SubmeterPreparedStatement(final SubmeterConnection connection, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) {
        this(connection, "", resultSetType, resultSetConcurrency, resultSetHoldability, false);
    }


    private SubmeterPreparedStatement(
            final SubmeterConnection connection, final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability, final boolean returnGeneratedKeys) {
        this.connection = connection;
        this.sql = sql;
        this.resultSetConcurrency = resultSetConcurrency;
        this.resultSetType = resultSetType;
        this.resultSetHoldability = resultSetHoldability;
        this.returnGeneratedKeys = returnGeneratedKeys;
        this.parserEngine = SQLParserEngineFactory.getSQLParserEngine("MySQL");
    }

    @Override
    protected boolean isAccumulate() {
        return false;
    }


    @Override
    public ResultSet executeQuery() throws SQLException {
        return getStatements().get(0)
                .executeQuery(sql);
    }

    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
        return getStatements().get(0)
                .executeQuery(sql);
    }

    @Override
    public int executeUpdate() throws SQLException {
        return 0;
    }

    @Override
    public boolean execute() throws SQLException {
        Connection connection1 = connection.getConnection();

        PreparedStatement preparedStatement1 = connection1.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
        getStatements().add(preparedStatement1);

        PreparedStatement preparedStatement2 = connection1.prepareStatement(sql.replace("sys_operation_log_1", "sys_operation_log_2"), resultSetType, resultSetConcurrency, resultSetHoldability);
        getStatements().add(preparedStatement2);
        List<Boolean> booleans = new ArrayList<>(getStatements().size());



        setParametersForStatements();
        replayMethodForStatements();





        forceExecuteTemplate.execute(getStatements(), (statement) -> {
            boolean execute = ((PreparedStatement) statement).execute();
            booleans.add(execute);
        });


        SQLStatement sqlStatement = parserEngine.parse(sql, false);
        Map<String, TableMetaData> tableMetaDataMap = new HashMap<>(1, 1);

        TableMetaData metaData1 = TableMetaDataLoader.load(connection.getDataSource(), "sys_operation_log_1", "MySQL");
        tableMetaDataMap.put("sys_operation_log_1", metaData1);
        TableMetaData metaData2 = TableMetaDataLoader.load(connection.getDataSource(), "sys_operation_log_2", "MySQL");
        tableMetaDataMap.put("sys_operation_log_2", metaData2);
        SchemaMetaData schemaMetaData = new SchemaMetaData(tableMetaDataMap);

        SQLStatementContext sqlStatementContext = SQLStatementContextFactory.newInstance(schemaMetaData, sql, getParameters(), sqlStatement);
        MySQLDatabaseType databaseType = new MySQLDatabaseType();
        ShardingDQLResultMerger shardingDQLResultMerger = new ShardingDQLResultMerger(databaseType);


        List<ResultSet> resultSets = getResultSets();
        List<QueryResult> queryResults = getQueryResults(resultSets);
        MergedResult mergedResult = shardingDQLResultMerger.merge(queryResults, sqlStatementContext, schemaMetaData);
        SubmeterResult result = new SubmeterResult(resultSets, mergedResult, sqlStatementContext, this);


        currentResultSet = result;

        return booleans.get(0);
    }


    private void setParametersForStatements() {
        for (int i = 0; i < getStatements().size(); i++) {
            replaySetParameter((PreparedStatement) getStatements().get(i), getParameters());
        }
    }
    private void replayMethodForStatements() {
        for (Statement each : getStatements()) {
            replayMethodsInvocation(each);
        }
    }

    private void clearPrevious() throws SQLException {
        getStatements().clear();
        getParameters().clear();
    }

    @Override
    public void addBatch() throws SQLException {

    }

    @Override
    public ResultSet getResultSet() throws SQLException {
        if (null != currentResultSet) {
            return currentResultSet;
        }
        if (getStatements().size() > 0) {


            return getStatements().get(0).getResultSet();
        } else {
            return null;
        }
    }

    private List<ResultSet> getResultSets() throws SQLException {
        List<ResultSet> result = new ArrayList<>(getStatements().size());
        for (Statement each : getStatements()) {
            result.add(each.getResultSet());
        }
        return result;
    }


    private List<QueryResult> getQueryResults(final List<ResultSet> resultSets) throws SQLException {
        List<QueryResult> result = new ArrayList<>(resultSets.size());
        for (ResultSet each : resultSets) {
            if (null != each) {
                result.add(new StreamQueryResult(each));
            }
        }
        return result;
    }

    @Override
    public int getResultSetConcurrency() throws SQLException {
        return resultSetConcurrency;
    }

    @Override
    public int getResultSetType() throws SQLException {
        return resultSetType;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return connection;
    }

    @Override
    public ResultSet getGeneratedKeys() throws SQLException {
        return null;
    }

    @Override
    public int getResultSetHoldability() throws SQLException {
        return resultSetHoldability;
    }

}
