package tech.easyflow.datacenter.connector.impl;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.row.Row;
import org.springframework.stereotype.Component;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.datacenter.connector.dialect.MysqlSqlDialect;
import tech.easyflow.datacenter.connector.support.AbstractJdbcConnector;
import tech.easyflow.datacenter.connector.support.WriteReceiptSupport;
import tech.easyflow.datacenter.entity.DatacenterTable;
import tech.easyflow.datacenter.entity.DatacenterTableField;
import tech.easyflow.datacenter.execution.model.DatacenterQueryRequest;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.datacenter.meta.enums.DatacenterCapability;
import tech.easyflow.datacenter.meta.enums.DatacenterSourceType;

import javax.sql.DataSource;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Component
public class ProjectMysqlConnector extends AbstractJdbcConnector {

    private final DataSource dataSource;
    private final MysqlSqlDialect dialect = new MysqlSqlDialect();

    public ProjectMysqlConnector(DataSource dataSource) {
        super(DatacenterSourceType.PROJECT_MYSQL, new MysqlSqlDialect(), EnumSet.of(
                DatacenterCapability.TEST_CONNECTION,
                DatacenterCapability.BROWSE_METADATA,
                DatacenterCapability.READ_QUERY,
                DatacenterCapability.WRITE_MUTATION
        ));
        this.dataSource = dataSource;
    }

    @Override
    protected <T> T withConnection(DatacenterSource source, boolean cacheable, JdbcCallback<T> callback) throws Exception {
        try (Connection connection = dataSource.getConnection()) {
            return callback.apply(connection);
        }
    }

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

    @Override
    public Page<Row> queryPage(DatacenterSource source, DatacenterTable table, DatacenterQueryRequest request) {
        return super.queryPage(source, table, request);
    }

    @Override
    public void saveRow(DatacenterSource source, DatacenterTable table, JSONObject data, LoginAccount account) {
        List<DatacenterTableField> writableFields = table.getFields().stream()
                .filter(field -> field.getWritable() == null || field.getWritable() == 1)
                .collect(Collectors.toList());
        Object id = data.get("id");
        try (Connection connection = dataSource.getConnection()) {
            if (id == null) {
                List<String> columns = new ArrayList<>();
                List<Object> values = new ArrayList<>();
                for (DatacenterTableField field : writableFields) {
                    Object value = data.get(field.getFieldName());
                    if (value != null) {
                        columns.add(field.getFieldName());
                        values.add(value);
                    }
                }
                if (columns.isEmpty()) {
                    throw new BusinessException("没有可写字段");
                }
                String sql = "INSERT INTO " + dialect.qualifyTable(source.getDatabaseName(), resolvePhysicalTableName(table))
                        + " (" + columns.stream().map(dialect::quoteIdentifier).collect(Collectors.joining(",")) + ") VALUES ("
                        + columns.stream().map(item -> "?").collect(Collectors.joining(",")) + ")";
                try (PreparedStatement statement = connection.prepareStatement(sql)) {
                    for (int i = 0; i < values.size(); i++) {
                        statement.setObject(i + 1, values.get(i));
                    }
                    statement.executeUpdate();
                }
                return;
            }
            List<String> setClauses = new ArrayList<>();
            List<Object> values = new ArrayList<>();
            for (DatacenterTableField field : writableFields) {
                if (!data.containsKey(field.getFieldName())) {
                    continue;
                }
                setClauses.add(dialect.quoteIdentifier(field.getFieldName()) + " = ?");
                values.add(data.get(field.getFieldName()));
            }
            if (setClauses.isEmpty()) {
                return;
            }
            String sql = "UPDATE " + dialect.qualifyTable(source.getDatabaseName(), resolvePhysicalTableName(table))
                    + " SET " + String.join(",", setClauses)
                    + " WHERE " + dialect.quoteIdentifier("id") + " = ?";
            values.add(id);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                for (int i = 0; i < values.size(); i++) {
                    statement.setObject(i + 1, values.get(i));
                }
                statement.executeUpdate();
            }
        } catch (Exception ex) {
            throw new BusinessException("项目 MySQL 写入失败: " + ex.getMessage());
        }
    }

    /**
     * 使用单个连接按相同 SQL 连续分组批量保存数据。
     *
     * @param source 数据源
     * @param table 数据表
     * @param rows 待保存数据行
     * @param account 当前操作账号
     * @param batchSize 单批最大行数
     */
    @Override
    public void saveRows(DatacenterSource source,
                         DatacenterTable table,
                         List<JSONObject> rows,
                         LoginAccount account,
                         int batchSize) {
        if (rows == null || rows.isEmpty()) {
            return;
        }
        try (Connection connection = dataSource.getConnection()) {
            saveRows(connection, source, table, rows, batchSize);
        } catch (Exception ex) {
            throw new BusinessException("项目 MySQL 批量写入失败: " + ex.getMessage());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean saveRowsIdempotently(
            DatacenterSource source,
            DatacenterTable table,
            List<JSONObject> rows,
            LoginAccount account,
            int batchSize,
            String receiptKey,
            String payloadHash) {
        if (StrUtil.isBlank(receiptKey)) {
            saveRows(source, table, rows, account, batchSize);
            return true;
        }
        try (Connection connection = dataSource.getConnection()) {
            boolean originalAutoCommit = connection.getAutoCommit();
            connection.setAutoCommit(false);
            try {
                if (hasMatchingReceipt(connection, receiptKey, payloadHash)) {
                    connection.rollback();
                    return false;
                }
                int effectiveBatchSize = Math.max(1, batchSize);
                for (int offset = 0; offset < rows.size(); offset += effectiveBatchSize) {
                    int end = Math.min(rows.size(), offset + effectiveBatchSize);
                    List<PendingRow> batch = pendingRows(
                            connection,
                            rows,
                            offset,
                            end,
                            receiptKey,
                            payloadHash);
                    if (batch.isEmpty()) {
                        connection.rollback();
                        continue;
                    }
                    try {
                        insertReceiptsBatch(connection, batch, payloadHash);
                        saveRows(
                                connection,
                                source,
                                table,
                                batch.stream()
                                        .map(PendingRow::row)
                                        .collect(Collectors.toList()),
                                effectiveBatchSize);
                        connection.commit();
                    } catch (Exception batchError) {
                        connection.rollback();
                        /*
                         * 批失败才逐行回放，精确保留旧实现“失败前行已提交、失败后不再执行”
                         * 的可观察语义，同时让正常路径按 batchSize 真正批量提交。
                         */
                        replayRowsIndividually(
                                connection,
                                source,
                                table,
                                batch,
                                payloadHash);
                    }
                }
                if (!insertReceipt(connection, receiptKey, payloadHash)) {
                    connection.rollback();
                    return false;
                }
                connection.commit();
                return true;
            } catch (Exception error) {
                connection.rollback();
                throw error;
            } finally {
                connection.setAutoCommit(originalAutoCommit);
            }
        } catch (Exception error) {
            throw new BusinessException("项目 MySQL 幂等批量写入失败: " + error.getMessage());
        }
    }

    /**
     * 批量读取子回执并筛出尚未写入的行。
     *
     * @param connection JDBC 连接
     * @param rows 全部数据行
     * @param startInclusive 批起始下标
     * @param endExclusive 批结束下标
     * @param receiptKey 根回执键
     * @param payloadHash 负载摘要
     * @return 尚未提交的行
     * @throws SQLException 查询失败或回执负载冲突
     */
    private List<PendingRow> pendingRows(
            Connection connection,
            List<JSONObject> rows,
            int startInclusive,
            int endExclusive,
            String receiptKey,
            String payloadHash) throws SQLException {
        List<PendingRow> candidates =
                new ArrayList<>(endExclusive - startInclusive);
        for (int rowIndex = startInclusive;
             rowIndex < endExclusive;
             rowIndex++) {
            candidates.add(new PendingRow(
                    WriteReceiptSupport.childKey(receiptKey, rowIndex),
                    rows.get(rowIndex)));
        }
        String placeholders = candidates.stream()
                .map(candidate -> "?")
                .collect(Collectors.joining(","));
        String sql = "SELECT idempotency_key, payload_hash "
                + "FROM tb_datacenter_write_receipt "
                + "WHERE idempotency_key IN (" + placeholders + ")";
        Map<String, String> existing = new HashMap<>();
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            for (int index = 0; index < candidates.size(); index++) {
                statement.setString(index + 1, candidates.get(index).receiptKey());
            }
            try (ResultSet resultSet = statement.executeQuery()) {
                while (resultSet.next()) {
                    existing.put(resultSet.getString(1), resultSet.getString(2));
                }
            }
        }
        List<PendingRow> pending = new ArrayList<>(candidates.size());
        for (PendingRow candidate : candidates) {
            String existingHash = existing.get(candidate.receiptKey());
            if (existingHash == null) {
                pending.add(candidate);
            } else if (!java.util.Objects.equals(payloadHash, existingHash)) {
                throw new SQLException(
                        "相同幂等键对应的数据内容不一致", "23000");
            }
        }
        return pending;
    }

    /**
     * 在当前事务中批量创建子回执。
     *
     * @param connection JDBC 连接
     * @param rows 待写行
     * @param payloadHash 负载摘要
     * @throws SQLException 回执批写失败
     */
    private void insertReceiptsBatch(
            Connection connection,
            List<PendingRow> rows,
            String payloadHash) throws SQLException {
        String sql = "INSERT INTO tb_datacenter_write_receipt "
                + "(idempotency_key, payload_hash, created) "
                + "VALUES (?, ?, CURRENT_TIMESTAMP)";
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            for (PendingRow row : rows) {
                statement.setString(1, row.receiptKey());
                statement.setString(2, payloadHash);
                statement.addBatch();
            }
            statement.executeBatch();
        }
    }

    /**
     * 批失败后逐行回放，定位首个业务失败并保留旧部分成功边界。
     *
     * @param connection JDBC 连接
     * @param source 数据源
     * @param table 数据表
     * @param rows 本批待写行
     * @param payloadHash 负载摘要
     * @throws Exception 首个真实行写入失败
     */
    private void replayRowsIndividually(
            Connection connection,
            DatacenterSource source,
            DatacenterTable table,
            List<PendingRow> rows,
            String payloadHash) throws Exception {
        for (PendingRow pendingRow : rows) {
            if (!insertReceipt(
                    connection,
                    pendingRow.receiptKey(),
                    payloadHash)) {
                connection.rollback();
                continue;
            }
            try {
                saveRows(
                        connection,
                        source,
                        table,
                        List.of(pendingRow.row()),
                        1);
                connection.commit();
            } catch (Exception rowError) {
                connection.rollback();
                throw rowError;
            }
        }
    }

    /**
     * 待写行及其稳定子回执键。
     *
     * @param receiptKey 子回执键
     * @param row 数据行
     */
    private record PendingRow(String receiptKey, JSONObject row) {
    }

    /**
     * 检查整次写入回执是否已经存在并校验负载。
     *
     * @param connection JDBC 连接
     * @param receiptKey 回执键
     * @param payloadHash 负载摘要
     * @return 相同负载的回执存在时为 {@code true}
     * @throws SQLException 数据库访问失败或负载冲突
     */
    private boolean hasMatchingReceipt(
            Connection connection, String receiptKey, String payloadHash)
            throws SQLException {
        String querySql = "SELECT payload_hash FROM tb_datacenter_write_receipt "
                + "WHERE idempotency_key = ?";
        try (PreparedStatement statement = connection.prepareStatement(querySql)) {
            statement.setString(1, receiptKey);
            try (ResultSet resultSet = statement.executeQuery()) {
                if (!resultSet.next()) {
                    return false;
                }
                if (!java.util.Objects.equals(payloadHash, resultSet.getString(1))) {
                    throw new SQLException(
                            "相同幂等键对应的数据内容不一致", "23000");
                }
                return true;
            }
        }
    }

    /**
     * 在当前连接中写入唯一回执。
     *
     * @param connection JDBC 连接
     * @param receiptKey 回执键
     * @param payloadHash 负载摘要
     * @return 新建回执时为 {@code true}，相同负载回执已存在时为 {@code false}
     * @throws SQLException 数据库访问失败或负载摘要冲突
     */
    private boolean insertReceipt(
            Connection connection, String receiptKey, String payloadHash) throws SQLException {
        String insertSql = "INSERT INTO tb_datacenter_write_receipt "
                + "(idempotency_key, payload_hash, created) VALUES (?, ?, CURRENT_TIMESTAMP)";
        try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
            statement.setString(1, receiptKey);
            statement.setString(2, payloadHash);
            statement.executeUpdate();
            return true;
        } catch (SQLException duplicate) {
            if (!"23000".equals(duplicate.getSQLState()) && duplicate.getErrorCode() != 1062) {
                throw duplicate;
            }
            String querySql = "SELECT payload_hash FROM tb_datacenter_write_receipt "
                    + "WHERE idempotency_key = ?";
            try (PreparedStatement statement = connection.prepareStatement(querySql)) {
                statement.setString(1, receiptKey);
                try (ResultSet resultSet = statement.executeQuery()) {
                    if (!resultSet.next()) {
                        throw duplicate;
                    }
                    if (!java.util.Objects.equals(payloadHash, resultSet.getString(1))) {
                        throw new SQLException("相同幂等键对应的数据内容不一致", "23000");
                    }
                    return false;
                }
            }
        }
    }

    /**
     * 在给定连接上执行完整批量，供普通和事务幂等写入复用。
     *
     * @param connection JDBC 连接
     * @param source 数据源
     * @param table 数据表
     * @param rows 待保存行
     * @param batchSize 单批最大行数
     * @throws Exception JDBC 批处理失败
     */
    private void saveRows(
            Connection connection,
            DatacenterSource source,
            DatacenterTable table,
            List<JSONObject> rows,
            int batchSize) throws Exception {
        List<DatacenterTableField> writableFields = table.getFields().stream()
                .filter(field -> field.getWritable() == null || field.getWritable() == 1)
                .collect(Collectors.toList());
        int effectiveBatchSize = Math.max(1, batchSize);
        List<SqlMutation> batch = new ArrayList<>(Math.min(rows.size(), effectiveBatchSize));
        String batchSql = null;
        for (JSONObject row : rows) {
            SqlMutation mutation = buildMutation(source, table, writableFields, row);
            if (mutation == null) {
                continue;
            }
            if (batchSql != null
                    && (!batchSql.equals(mutation.sql) || batch.size() >= effectiveBatchSize)) {
                executeBatch(connection, batchSql, batch);
                batch.clear();
            }
            batchSql = mutation.sql;
            batch.add(mutation);
        }
        if (!batch.isEmpty()) {
            executeBatch(connection, batchSql, batch);
        }
    }

    /**
     * 构建单行参数化写入。
     *
     * @param source 数据源
     * @param table 数据表
     * @param writableFields 可写字段
     * @param data 数据行
     * @return SQL 与参数；无可更新字段时返回 null
     */
    private SqlMutation buildMutation(DatacenterSource source,
                                      DatacenterTable table,
                                      List<DatacenterTableField> writableFields,
                                      JSONObject data) {
        Object id = data.get("id");
        if (id == null) {
            List<String> columns = new ArrayList<>();
            List<Object> values = new ArrayList<>();
            for (DatacenterTableField field : writableFields) {
                Object value = data.get(field.getFieldName());
                if (value != null) {
                    columns.add(field.getFieldName());
                    values.add(value);
                }
            }
            if (columns.isEmpty()) {
                throw new BusinessException("没有可写字段");
            }
            String sql = "INSERT INTO "
                    + dialect.qualifyTable(source.getDatabaseName(), resolvePhysicalTableName(table))
                    + " (" + columns.stream().map(dialect::quoteIdentifier).collect(Collectors.joining(","))
                    + ") VALUES (" + columns.stream().map(item -> "?").collect(Collectors.joining(",")) + ")";
            return new SqlMutation(sql, values);
        }

        List<String> setClauses = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        for (DatacenterTableField field : writableFields) {
            if (!data.containsKey(field.getFieldName())) {
                continue;
            }
            setClauses.add(dialect.quoteIdentifier(field.getFieldName()) + " = ?");
            values.add(data.get(field.getFieldName()));
        }
        if (setClauses.isEmpty()) {
            return null;
        }
        String sql = "UPDATE "
                + dialect.qualifyTable(source.getDatabaseName(), resolvePhysicalTableName(table))
                + " SET " + String.join(",", setClauses)
                + " WHERE " + dialect.quoteIdentifier("id") + " = ?";
        values.add(id);
        return new SqlMutation(sql, values);
    }

    /**
     * 执行同构 SQL 批次。
     *
     * @param connection 数据库连接
     * @param sql 参数化 SQL
     * @param mutations 待执行参数
     * @throws Exception JDBC 批处理失败时抛出
     */
    private void executeBatch(Connection connection, String sql, List<SqlMutation> mutations) throws Exception {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            for (SqlMutation mutation : mutations) {
                for (int index = 0; index < mutation.parameters.size(); index++) {
                    statement.setObject(index + 1, mutation.parameters.get(index));
                }
                statement.addBatch();
            }
            statement.executeBatch();
        }
    }

    /**
     * 参数化写入描述。
     */
    private static final class SqlMutation {

        private final String sql;
        private final List<Object> parameters;

        /**
         * 创建参数化写入。
         *
         * @param sql SQL 文本
         * @param parameters SQL 参数
         */
        private SqlMutation(String sql, List<Object> parameters) {
            this.sql = sql;
            this.parameters = parameters;
        }
    }

    @Override
    public void deleteRow(DatacenterSource source, DatacenterTable table, BigInteger id, LoginAccount account) {
        String sql = "DELETE FROM " + dialect.qualifyTable(source.getDatabaseName(), resolvePhysicalTableName(table))
                + " WHERE " + dialect.quoteIdentifier("id") + " = ?";
        try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setObject(1, id);
            statement.executeUpdate();
        } catch (Exception ex) {
            throw new BusinessException("项目 MySQL 删除失败: " + ex.getMessage());
        }
    }

    @Override
    protected String resolveCatalogName(DatacenterSource source, String requestedCatalogName) {
        return StrUtil.blankToDefault(requestedCatalogName, source.getDatabaseName());
    }
}
