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.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper;
import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.OperationTypeEnum;
import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;

/**
 * 数据库运维任务服务实现。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperationTaskMapper, DatabaseOperationTask> implements DatabaseOperationTaskService {

    private final DatabaseConnectionService databaseConnectionService;
    private final DatabaseBackupFileService databaseBackupFileService;
    private final DatabaseOperatorRegistry databaseOperatorRegistry;
    private final ObjectMapper objectMapper;
    @Resource(name = "dbmsTaskExecutorService")
    private ExecutorService dbmsTaskExecutorService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DatabaseTaskCreateVO createBackupTask(DatabaseBackupParam.CreateParam param) {
        DatabaseConnection connection = databaseConnectionService.requireEnabled(param.getConnectionId());
        validateBackupParam(param, connection);
        if (existsRunningTask(connection.getId())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "当前连接存在运行中的任务");
        }
        DatabaseOperationTask task = buildBackupTask(param, connection);
        this.save(task);
        dbmsTaskExecutorService.submit(() -> executeBackupTask(task.getId(), param));
        return toCreateVO(task);
    }

    @Override
    public Page<DatabaseTaskVO> listBackupTasks(DatabaseBackupParam.TaskQueryParam param) {
        DatabaseBackupParam.TaskQueryParam query = param == null ? new DatabaseBackupParam.TaskQueryParam() : param;
        LambdaQueryWrapper<DatabaseOperationTask> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(DatabaseOperationTask::getState, DatabaseOpsConst.STATE_ENABLED)
                .eq(DatabaseOperationTask::getOperationType, OperationTypeEnum.BACKUP.name())
                .eq(StrUtil.isNotBlank(query.getConnectionId()), DatabaseOperationTask::getConnectionId, query.getConnectionId())
                .eq(StrUtil.isNotBlank(query.getTaskStatus()), DatabaseOperationTask::getTaskStatus, query.getTaskStatus())
                .orderByDesc(DatabaseOperationTask::getCreateTime);
        Page<DatabaseOperationTask> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
        Page<DatabaseTaskVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        result.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList()));
        return result;
    }

    @Override
    public DatabaseTaskVO getStatus(String taskId) {
        return toVO(requireEnabledTask(taskId));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean stopBackupTask(DatabaseBackupParam.StopParam param) {
        DatabaseOperationTask task = requireEnabledTask(param.getTaskId());
        if (!OperationTypeEnum.BACKUP.name().equals(task.getOperationType())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "仅支持停止备份任务");
        }
        if (!TaskStatusEnum.WAITING.name().equals(task.getTaskStatus())
                && !TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "仅等待中或运行中的任务允许停止");
        }
        task.setTaskStatus(TaskStatusEnum.CANCELLED.name());
        task.setResultMessage("用户请求停止备份任务");
        task.setFinishedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        return this.updateById(task);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DatabaseTaskCreateVO restartBackupTask(DatabaseBackupParam.RestartParam param) {
        DatabaseOperationTask sourceTask = requireEnabledTask(param.getTaskId());
        if (!OperationTypeEnum.BACKUP.name().equals(sourceTask.getOperationType())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "仅支持重新开始备份任务");
        }
        if (!TaskStatusEnum.FAIL.name().equals(sourceTask.getTaskStatus())
                && !TaskStatusEnum.CANCELLED.name().equals(sourceTask.getTaskStatus())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "仅失败或已取消的任务允许重新开始");
        }
        DatabaseBackupParam.CreateParam createParam = readCreateParam(sourceTask.getRequestParamJson());
        createParam.setTemporaryPassword(param.getTemporaryPassword());
        return createBackupTask(createParam);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteTask(String taskId, String confirmText) {
        if (!DatabaseOpsConst.CONFIRM_DELETE.equals(confirmText)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "确认文案不正确");
        }
        DatabaseOperationTask task = requireEnabledTask(taskId);
        if (TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus()) || TaskStatusEnum.WAITING.name().equals(task.getTaskStatus())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "运行中的任务不能删除");
        }
        task.setState(DatabaseOpsConst.STATE_DELETED);
        task.setUpdateTime(LocalDateTime.now());
        return this.updateById(task);
    }

    @Override
    public boolean existsRunningTask(String connectionId) {
        return this.lambdaQuery()
                .eq(DatabaseOperationTask::getConnectionId, connectionId)
                .eq(DatabaseOperationTask::getState, DatabaseOpsConst.STATE_ENABLED)
                .in(DatabaseOperationTask::getTaskStatus, Arrays.asList(TaskStatusEnum.WAITING.name(), TaskStatusEnum.RUNNING.name()))
                .count() > 0;
    }

    private void executeBackupTask(String taskId, DatabaseBackupParam.CreateParam param) {
        DatabaseOperationTask task = this.getById(taskId);
        try {
            if (task == null || TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) {
                return;
            }
            markRunning(task);
            DatabaseConnection connection = databaseConnectionService.requireEnabled(task.getConnectionId());
            connection.setSchemaName(task.getSchemaName());
            String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword());
            DatabaseBackupOperator operator = databaseOperatorRegistry.getBackupOperator(connection.getDbType(), task.getBackupStrategy());
            DatabaseBackupFile backupFile = operator.executeBackup(task, connection, password, param);
            task = this.getById(taskId);
            if (task == null || TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) {
                return;
            }
            databaseBackupFileService.save(backupFile);
            markSuccess(task, "备份任务执行成功");
        } catch (Exception exception) {
            log.error("数据库备份任务失败，taskId={}", taskId, exception);
            task = this.getById(taskId);
            if (task != null && TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) {
                markCancelled(task, exception.getMessage());
                return;
            }
            markFail(task, exception.getMessage());
        }
    }

    private DatabaseOperationTask buildBackupTask(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
        DatabaseOperationTask task = new DatabaseOperationTask();
        task.setId(DatabaseOpsIdUtil.uuid());
        task.setTaskNo(DatabaseOpsIdUtil.taskNo("DBMSB"));
        task.setConnectionId(connection.getId());
        task.setDbType(connection.getDbType());
        task.setOperationType(OperationTypeEnum.BACKUP.name());
        task.setBackupStrategy(resolveBackupStrategy(param.getBackupStrategy(), connection.getDbType()));
        task.setTaskStatus(TaskStatusEnum.WAITING.name());
        task.setSchemaName(resolveSchemaName(param, connection));
        task.setTargetNamesJson(writeJson(param.getTargetNames()));
        task.setRequestParamJson(writeJsonWithoutPassword(param));
        task.setProgressPercent(BigDecimal.ZERO);
        task.setState(DatabaseOpsConst.STATE_ENABLED);
        task.setCreateTime(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        return task;
    }

    private void validateBackupParam(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
        if (param.getTargetNames() == null || param.getTargetNames().isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "备份表不能为空");
        }
        if (DatabaseOpsConst.DB_TYPE_ORACLE.equals(connection.getDbType())
                && StrUtil.isBlank(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()))) {
            throw new BusinessException(CommonResponseEnum.FAIL, "备份 Schema 不能为空");
        }
        if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType()) && StrUtil.isBlank(connection.getDatabaseName())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 数据库名不能为空");
        }
        String backupMode = StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT);
        if (BackupModeEnum.TIME_RANGE.name().equals(backupMode)
                && (param.getStartTime() == null || param.getEndTime() == null)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "按时间备份必须传入开始时间和结束时间");
        }
        if (BackupModeEnum.TIME_RANGE.name().equals(backupMode)
                && param.getStartTime() != null && param.getEndTime() != null
                && param.getStartTime().isAfter(param.getEndTime())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "开始时间不能晚于结束时间");
        }
        if (BackupModeEnum.SIZE_SPLIT.name().equals(backupMode)
                && (param.getMaxFileSizeMb() == null || param.getMaxFileSizeMb() <= 0)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "按大小分片必须传入大于 0 的文件大小");
        }
        if (BackupModeEnum.TIME_RANGE.name().equals(backupMode) && StrUtil.isBlank(param.getTimeColumn())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "按时间备份必须传入时间字段");
        }
        resolveBackupStrategy(param.getBackupStrategy(), connection.getDbType());
    }

    private String resolveBackupStrategy(String backupStrategy, String dbType) {
        String value = StrUtil.blankToDefault(backupStrategy,
                        DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? BackupStrategyEnum.DATA_PUMP.name() : BackupStrategyEnum.JDBC_EXPORT.name())
                .trim()
                .toUpperCase(Locale.ROOT);
        try {
            BackupStrategyEnum strategyEnum = BackupStrategyEnum.valueOf(value);
            if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(dbType) && BackupStrategyEnum.DATA_PUMP == strategyEnum) {
                throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 不支持 DATA_PUMP");
            }
            return strategyEnum.name();
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "不支持的备份策略：" + backupStrategy);
        }
    }

    private String resolveSchemaName(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
        if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) {
            return StrUtil.blankToDefault(param.getSchemaName(), connection.getDatabaseName());
        }
        return StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName());
    }

    private void markRunning(DatabaseOperationTask task) {
        task.setTaskStatus(TaskStatusEnum.RUNNING.name());
        task.setStartedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private void markSuccess(DatabaseOperationTask task, String message) {
        task.setTaskStatus(TaskStatusEnum.SUCCESS.name());
        task.setResultMessage(message);
        task.setProgressPercent(new BigDecimal("100.00"));
        task.setFinishedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private void markFail(DatabaseOperationTask task, String message) {
        if (task == null) {
            return;
        }
        task.setTaskStatus(TaskStatusEnum.FAIL.name());
        task.setResultMessage(message);
        task.setFinishedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private void markCancelled(DatabaseOperationTask task, String message) {
        task.setTaskStatus(TaskStatusEnum.CANCELLED.name());
        task.setResultMessage(StrUtil.blankToDefault(message, "备份任务已停止"));
        task.setFinishedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private DatabaseOperationTask requireEnabledTask(String taskId) {
        DatabaseOperationTask task = this.getById(taskId);
        if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除");
        }
        return task;
    }

    private String writeJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
        }
    }

    private String writeJsonWithoutPassword(DatabaseBackupParam.CreateParam param) {
        DatabaseBackupParam.CreateParam copy = new DatabaseBackupParam.CreateParam();
        BeanUtil.copyProperties(param, copy);
        copy.setTemporaryPassword(null);
        return writeJson(copy);
    }

    private DatabaseBackupParam.CreateParam readCreateParam(String requestParamJson) {
        try {
            return objectMapper.readValue(requestParamJson, DatabaseBackupParam.CreateParam.class);
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
        }
    }

    private DatabaseTaskCreateVO toCreateVO(DatabaseOperationTask task) {
        DatabaseTaskCreateVO vo = new DatabaseTaskCreateVO();
        vo.setTaskId(task.getId());
        vo.setTaskNo(task.getTaskNo());
        vo.setTaskStatus(task.getTaskStatus());
        return vo;
    }

    private DatabaseTaskVO toVO(DatabaseOperationTask task) {
        DatabaseTaskVO vo = new DatabaseTaskVO();
        BeanUtil.copyProperties(task, vo);
        return vo;
    }
}
