package com.njcn.gather.systemops.database.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DatabasePasswordComponent;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.mapper.DatabaseConnectionMapper;
import com.njcn.gather.systemops.database.pojo.param.DatabaseConnectionParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseConnectionVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator;
import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

/**
 * 数据库连接配置服务实现。
 */
@Service
@RequiredArgsConstructor
public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectionMapper, DatabaseConnection> implements DatabaseConnectionService {

    private final DatabasePasswordComponent databasePasswordComponent;
    private final DatabaseOperatorRegistry databaseOperatorRegistry;
    private final ObjectProvider<DatabaseOperationTaskService> databaseOperationTaskServiceProvider;

    @Override
    public Page<DatabaseConnectionVO> listConnections(DatabaseConnectionParam.QueryParam queryParam) {
        DatabaseConnectionParam.QueryParam query = queryParam == null ? new DatabaseConnectionParam.QueryParam() : queryParam;
        LambdaQueryWrapper<DatabaseConnection> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED)
                .like(StrUtil.isNotBlank(query.getConnectionName()), DatabaseConnection::getConnectionName, query.getConnectionName())
                .eq(StrUtil.isNotBlank(query.getDbType()), DatabaseConnection::getDbType, query.getDbType())
                .like(StrUtil.isNotBlank(query.getSchemaName()), DatabaseConnection::getSchemaName, query.getSchemaName())
                .orderByDesc(DatabaseConnection::getUpdateTime);
        Page<DatabaseConnection> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
        Page<DatabaseConnectionVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        result.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList()));
        return result;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addConnection(DatabaseConnectionParam param) {
        DatabaseConnection connection = new DatabaseConnection();
        fillConnection(connection, param, true);
        checkConnectionNameUnique(connection.getConnectionName());
        connection.setId(DatabaseOpsIdUtil.uuid());
        connection.setState(DatabaseOpsConst.STATE_ENABLED);
        connection.setCreateTime(LocalDateTime.now());
        connection.setUpdateTime(LocalDateTime.now());
        return this.save(connection);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateConnection(DatabaseConnectionParam.UpdateParam param) {
        DatabaseConnection connection = requireEnabled(param.getId());
        fillConnection(connection, param, false);
        connection.setUpdateTime(LocalDateTime.now());
        return this.updateById(connection);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteConnection(DatabaseConnectionParam.DeleteParam param) {
        requireEnabled(param.getId());
        if (databaseOperationTaskServiceProvider.getObject().existsRunningTask(param.getId())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "存在运行中的任务，不能删除连接");
        }
        return this.lambdaUpdate()
                .set(DatabaseConnection::getState, DatabaseOpsConst.STATE_DELETED)
                .set(DatabaseConnection::getUpdateTime, LocalDateTime.now())
                .eq(DatabaseConnection::getId, param.getId())
                .update();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DatabaseTestResultVO testConnection(DatabaseConnectionParam.TestParam param) {
        DatabaseConnection connection = resolveTestConnection(param);
        DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType());
        DatabaseTestResultVO result = operator.test(connection, resolvePassword(connection, param.getTemporaryPassword()));
        if (StrUtil.isNotBlank(connection.getId())) {
            updateLastTestResult(connection.getId(), result);
        }
        return result;
    }

    @Override
    public List<DatabaseTableVO> listTables(DatabaseConnectionParam.TablesParam param) {
        DatabaseConnection connection = requireEnabled(param.getConnectionId());
        try {
            DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType());
            String password = resolveTablesPassword(connection, param);
            return operator.listTables(connection, password,
                    resolveSchemaOrDatabase(param, connection));
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage());
        }
    }

    @Override
    public DatabaseConnection requireEnabled(String connectionId) {
        if (StrUtil.isBlank(connectionId)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "连接 ID 不能为空");
        }
        DatabaseConnection connection = this.lambdaQuery()
                .eq(DatabaseConnection::getId, connectionId)
                .eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED)
                .one();
        if (connection == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "数据库连接不存在或已删除");
        }
        return connection;
    }

    @Override
    public String resolvePassword(DatabaseConnection connection, String temporaryPassword) {
        try {
            return databasePasswordComponent.resolveRuntimePassword(connection.getPasswordCipher(), temporaryPassword);
        } catch (IllegalArgumentException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage());
        }
    }

    private DatabaseConnection resolveTestConnection(DatabaseConnectionParam.TestParam param) {
        if (StrUtil.isNotBlank(param.getConnectionId())) {
            DatabaseConnection savedConnection = requireEnabled(param.getConnectionId());
            if (param.getConnection() == null) {
                return savedConnection;
            }
            DatabaseConnection connection = new DatabaseConnection();
            fillConnection(connection, param.getConnection(), true, true);
            connection.setId(savedConnection.getId());
            if (StrUtil.isBlank(param.getConnection().getPassword())) {
                // 已有连接测试编辑后参数时，未传密码则复用库里保存的密码。
                connection.setPasswordCipher(savedConnection.getPasswordCipher());
            }
            return connection;
        }
        if (param.getConnection() == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "连接测试参数不能为空");
        }
        DatabaseConnection connection = new DatabaseConnection();
        fillConnection(connection, param.getConnection(), true, StrUtil.isNotBlank(param.getTemporaryPassword()));
        return connection;
    }

    private void updateLastTestResult(String connectionId, DatabaseTestResultVO result) {
        this.lambdaUpdate()
                .set(DatabaseConnection::getLastTestStatus, Boolean.TRUE.equals(result.getSuccess()) ? "SUCCESS" : "FAIL")
                .set(DatabaseConnection::getLastTestMessage, result.getMessage())
                .set(DatabaseConnection::getLastTestTime, LocalDateTime.now())
                .eq(DatabaseConnection::getId, connectionId)
                .update();
    }

    private String resolveTablesPassword(DatabaseConnection connection, DatabaseConnectionParam.TablesParam param) {
        if (StrUtil.isNotBlank(param.getTemporaryPassword())) {
            return param.getTemporaryPassword();
        }
        if (StrUtil.isNotBlank(param.getPassword())) {
            return param.getPassword();
        }
        if (param.getConnection() != null && StrUtil.isNotBlank(param.getConnection().getPassword())) {
            return param.getConnection().getPassword();
        }
        if (StrUtil.isNotBlank(param.getPasswordCipher())) {
            return databasePasswordComponent.resolveRuntimePassword(param.getPasswordCipher(), null);
        }
        return resolvePassword(connection, null);
    }

    private void fillConnection(DatabaseConnection connection, DatabaseConnectionParam param, boolean create) {
        fillConnection(connection, param, create, false);
    }

    private void fillConnection(DatabaseConnection connection, DatabaseConnectionParam param, boolean create,
                                boolean allowTemporaryPasswordOnly) {
        String dbType = resolveDbType(param.getDbType());
        validateConnectionParam(param, dbType);
        connection.setConnectionName(param.getConnectionName().trim());
        connection.setDbType(dbType);
        connection.setHost(param.getHost().trim());
        connection.setPort(param.getPort());
        connection.setConnectType(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? resolveConnectType(param.getConnectType()) : null);
        connection.setServiceName(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getServiceName()) : null);
        connection.setSid(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getSid()) : null);
        connection.setDatabaseName(DatabaseOpsConst.DB_TYPE_MYSQL.equals(dbType) ? trimToNull(param.getDatabaseName()) : null);
        connection.setSchemaName(trimToNull(param.getSchemaName()));
        connection.setUsername(param.getUsername().trim());
        connection.setSavePassword(param.getSavePassword() == null ? DatabaseOpsConst.SAVE_PASSWORD_YES : param.getSavePassword());
        if (connection.getSavePassword() != DatabaseOpsConst.SAVE_PASSWORD_YES
                && connection.getSavePassword() != DatabaseOpsConst.SAVE_PASSWORD_NO) {
            throw new BusinessException(CommonResponseEnum.FAIL, "savePassword 只能是 0 或 1");
        }
        if (DatabaseOpsConst.SAVE_PASSWORD_YES == connection.getSavePassword() && StrUtil.isNotBlank(param.getPassword())) {
            connection.setPasswordCipher(databasePasswordComponent.encrypt(param.getPassword()));
        }
        if (DatabaseOpsConst.SAVE_PASSWORD_NO == connection.getSavePassword()) {
            connection.setPasswordCipher(null);
        } else if (create && StrUtil.isBlank(param.getPassword()) && !allowTemporaryPasswordOnly) {
            throw new BusinessException(CommonResponseEnum.FAIL, "保存密码时密码不能为空");
        }
        connection.setDirectoryName(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getDirectoryName()) : null);
        connection.setDirectoryPath(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getDirectoryPath()) : null);
        connection.setExtraConfigJson(trimToNull(param.getExtraConfigJson()));
        connection.setRemark(param.getRemark());
    }

    private void validateConnectionParam(DatabaseConnectionParam param, String dbType) {
        if (DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType)) {
            String connectType = resolveConnectType(param.getConnectType());
            if (DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME.equals(connectType) && StrUtil.isBlank(param.getServiceName())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "SERVICE_NAME 连接方式下服务名不能为空");
            }
            if (DatabaseOpsConst.CONNECT_TYPE_SID.equals(connectType) && StrUtil.isBlank(param.getSid())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "SID 连接方式下 SID 不能为空");
            }
            return;
        }
        if (StrUtil.isBlank(param.getDatabaseName())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 数据库名不能为空");
        }
    }

    /**
     * 新增连接时，连接名称在有效记录中必须唯一。
     */
    private void checkConnectionNameUnique(String connectionName) {
        long count = this.lambdaQuery()
                .eq(DatabaseConnection::getConnectionName, connectionName)
                .eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED)
                .count();
        if (count > 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, "连接名称已存在");
        }
    }

    private String resolveConnectType(String connectType) {
        return StrUtil.blankToDefault(connectType, DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME).trim().toUpperCase(Locale.ROOT);
    }

    private String resolveDbType(String dbType) {
        String resolved = StrUtil.blankToDefault(dbType, DatabaseOpsConst.DB_TYPE_ORACLE).trim().toUpperCase(Locale.ROOT);
        if (!DatabaseOpsConst.DB_TYPE_ORACLE.equals(resolved) && !DatabaseOpsConst.DB_TYPE_MYSQL.equals(resolved)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "不支持的数据库类型：" + dbType);
        }
        return resolved;
    }

    private String resolveSchemaOrDatabase(DatabaseConnectionParam.TablesParam param, DatabaseConnection connection) {
        if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) {
            return StrUtil.blankToDefault(param.getSchemaName(), connection.getDatabaseName());
        }
        return param.getSchemaName();
    }

    private String trimToNull(String value) {
        return StrUtil.isBlank(value) ? null : value.trim();
    }

    private DatabaseConnectionVO toVO(DatabaseConnection connection) {
        DatabaseConnectionVO vo = new DatabaseConnectionVO();
        BeanUtil.copyProperties(connection, vo);
        return vo;
    }
}
